lox-airplay-sender 0.1.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.
Files changed (75) hide show
  1. package/README.md +85 -0
  2. package/dist/core/ap2_test.d.ts +1 -0
  3. package/dist/core/ap2_test.js +8 -0
  4. package/dist/core/atv.d.ts +16 -0
  5. package/dist/core/atv.js +215 -0
  6. package/dist/core/atvAuthenticator.d.ts +30 -0
  7. package/dist/core/atvAuthenticator.js +134 -0
  8. package/dist/core/audioOut.d.ts +30 -0
  9. package/dist/core/audioOut.js +80 -0
  10. package/dist/core/deviceAirtunes.d.ts +72 -0
  11. package/dist/core/deviceAirtunes.js +501 -0
  12. package/dist/core/devices.d.ts +50 -0
  13. package/dist/core/devices.js +209 -0
  14. package/dist/core/index.d.ts +47 -0
  15. package/dist/core/index.js +97 -0
  16. package/dist/core/rtsp.d.ts +12 -0
  17. package/dist/core/rtsp.js +1590 -0
  18. package/dist/core/srp.d.ts +14 -0
  19. package/dist/core/srp.js +128 -0
  20. package/dist/core/udpServers.d.ts +26 -0
  21. package/dist/core/udpServers.js +149 -0
  22. package/dist/esm/core/ap2_test.js +8 -0
  23. package/dist/esm/core/atv.js +215 -0
  24. package/dist/esm/core/atvAuthenticator.js +134 -0
  25. package/dist/esm/core/audioOut.js +80 -0
  26. package/dist/esm/core/deviceAirtunes.js +501 -0
  27. package/dist/esm/core/devices.js +209 -0
  28. package/dist/esm/core/index.js +97 -0
  29. package/dist/esm/core/rtsp.js +1590 -0
  30. package/dist/esm/core/srp.js +128 -0
  31. package/dist/esm/core/udpServers.js +149 -0
  32. package/dist/esm/homekit/credentials.js +100 -0
  33. package/dist/esm/homekit/encryption.js +82 -0
  34. package/dist/esm/homekit/number.js +47 -0
  35. package/dist/esm/homekit/tlv.js +97 -0
  36. package/dist/esm/index.js +265 -0
  37. package/dist/esm/package.json +1 -0
  38. package/dist/esm/utils/alac.js +62 -0
  39. package/dist/esm/utils/alacEncoder.js +34 -0
  40. package/dist/esm/utils/circularBuffer.js +124 -0
  41. package/dist/esm/utils/config.js +28 -0
  42. package/dist/esm/utils/http.js +148 -0
  43. package/dist/esm/utils/ntp.js +27 -0
  44. package/dist/esm/utils/numUtil.js +17 -0
  45. package/dist/esm/utils/packetPool.js +52 -0
  46. package/dist/esm/utils/util.js +9 -0
  47. package/dist/homekit/credentials.d.ts +30 -0
  48. package/dist/homekit/credentials.js +100 -0
  49. package/dist/homekit/encryption.d.ts +12 -0
  50. package/dist/homekit/encryption.js +82 -0
  51. package/dist/homekit/number.d.ts +7 -0
  52. package/dist/homekit/number.js +47 -0
  53. package/dist/homekit/tlv.d.ts +25 -0
  54. package/dist/homekit/tlv.js +97 -0
  55. package/dist/index.d.ts +109 -0
  56. package/dist/index.js +265 -0
  57. package/dist/utils/alac.d.ts +9 -0
  58. package/dist/utils/alac.js +62 -0
  59. package/dist/utils/alacEncoder.d.ts +14 -0
  60. package/dist/utils/alacEncoder.js +34 -0
  61. package/dist/utils/circularBuffer.d.ts +31 -0
  62. package/dist/utils/circularBuffer.js +124 -0
  63. package/dist/utils/config.d.ts +25 -0
  64. package/dist/utils/config.js +28 -0
  65. package/dist/utils/http.d.ts +19 -0
  66. package/dist/utils/http.js +148 -0
  67. package/dist/utils/ntp.d.ts +7 -0
  68. package/dist/utils/ntp.js +27 -0
  69. package/dist/utils/numUtil.d.ts +5 -0
  70. package/dist/utils/numUtil.js +17 -0
  71. package/dist/utils/packetPool.d.ts +25 -0
  72. package/dist/utils/packetPool.js +52 -0
  73. package/dist/utils/util.d.ts +2 -0
  74. package/dist/utils/util.js +9 -0
  75. package/package.json +62 -0
@@ -0,0 +1,501 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const node_dgram_1 = __importDefault(require("node:dgram"));
40
+ const node_events_1 = require("node:events");
41
+ const node_crypto_1 = __importDefault(require("node:crypto"));
42
+ const config_1 = __importDefault(require("../utils/config"));
43
+ const nu = __importStar(require("../utils/numUtil"));
44
+ const rtsp_1 = __importDefault(require("./rtsp"));
45
+ const udpServers_1 = __importDefault(require("./udpServers"));
46
+ const alac_1 = require("../utils/alac");
47
+ /**
48
+ * AirTunes (RAOP/AirPlay) device session.
49
+ * Handles RTSP control, UDP audio transport, optional ALAC encoding, and authentication hints.
50
+ */
51
+ const RTP_HEADER_SIZE = 12;
52
+ function formatLogArg(value) {
53
+ if (Buffer.isBuffer(value)) {
54
+ return `<buffer len=${value.length}>`;
55
+ }
56
+ if (value instanceof Uint8Array) {
57
+ return `<uint8 len=${value.length}>`;
58
+ }
59
+ if (typeof value === 'string') {
60
+ return value;
61
+ }
62
+ try {
63
+ return JSON.stringify(value);
64
+ }
65
+ catch {
66
+ return String(value);
67
+ }
68
+ }
69
+ function logLine(ctx, ...args) {
70
+ if (!args.length) {
71
+ return;
72
+ }
73
+ if (ctx?.log) {
74
+ const formatted = args.map(formatLogArg).join(' ');
75
+ ctx.log('debug', formatted);
76
+ return;
77
+ }
78
+ // eslint-disable-next-line no-console
79
+ console.log(...args);
80
+ }
81
+ class BufferWithNames {
82
+ size;
83
+ buffer = [];
84
+ constructor(size) {
85
+ this.size = size;
86
+ }
87
+ add(name, item) {
88
+ while (this.buffer.length > this.size) {
89
+ this.buffer.shift();
90
+ }
91
+ this.buffer.push([name, item]);
92
+ }
93
+ getLatestNamed(name) {
94
+ for (let i = this.buffer.length - 1; i >= 0; i -= 1) {
95
+ if (this.buffer[i][0] === name) {
96
+ return this.buffer[i][1];
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ }
102
+ /**
103
+ * Construct a RAOP/AirPlay device handler.
104
+ * Accepts discovery TXT/flags and wires RTSP handshake to UDP audio.
105
+ */
106
+ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
107
+ node_events_1.EventEmitter.call(this);
108
+ if (!host) {
109
+ throw new Error('host is mandatory');
110
+ }
111
+ this.audioPacketHistory = new BufferWithNames(100);
112
+ this.udpServers = new udpServers_1.default();
113
+ this.audioOut = audioOut;
114
+ this.type = 'airtunes';
115
+ this.options = options;
116
+ this.log = options?.log;
117
+ this.logLine = (...args) => logLine(this, ...args);
118
+ this.host = host;
119
+ this.port = Number(options?.port ?? 5000);
120
+ this.key = this.host + ':' + this.port;
121
+ this.mode = mode; // Homepods with or without passcode
122
+ // if (options.password != null && compatMode === true) {
123
+ // this.mode = 1; // Airport / Shairport passcode mode
124
+ // this.mode = 2 // MFi mode
125
+ // }
126
+ this.forceAlac = options.forceAlac ?? true;
127
+ // this.skipAutoVolume = options.skipAutoVolume ?? false
128
+ this.statusflags = [];
129
+ this.alacEncoding = options?.alacEncoding ?? true;
130
+ this.inputCodec = options?.inputCodec ?? 'pcm';
131
+ this.airplay2 = options?.airplay2 ?? false;
132
+ this.txt = Array.isArray(txt) ? txt : txt ? [String(txt)] : [];
133
+ this.borkedshp = false;
134
+ if (this.airplay2 && this.mode === 0) {
135
+ this.mode = 2;
136
+ }
137
+ // console.debug('airplay txt', this.txt, 'port', this.port);
138
+ let a = this.txt.filter((u) => String(u).startsWith('et='));
139
+ if ((a[0] ?? '').includes('4')) {
140
+ this.mode = 2;
141
+ }
142
+ let b = this.txt.filter((u) => String(u).startsWith('cn='));
143
+ if (!this.forceAlac) {
144
+ if ((b[0] ?? '').includes('0')) {
145
+ this.alacEncoding = false;
146
+ }
147
+ }
148
+ let c = this.txt.filter((u) => String(u).startsWith('sf='));
149
+ this.statusflags = c[0] ? parseInt(c[0].substring(3)).toString(2).split('') : [];
150
+ if (c.length == 0) {
151
+ c = this.txt.filter((u) => String(u).startsWith('flags='));
152
+ this.statusflags = c[0] ? parseInt(c[0].substring(6)).toString(2).split('') : [];
153
+ }
154
+ this.needPassword = false;
155
+ this.needPin = false;
156
+ this.transient = false;
157
+ let d = this.txt.filter((u) => String(u).startsWith('features='));
158
+ if (d.length === 0)
159
+ d = this.txt.filter((u) => String(u).startsWith('ft='));
160
+ const features_set = d.length > 0 ? d[0].substring(d[0].indexOf('=') + 1).split(',') : [];
161
+ this.features = [
162
+ ...(features_set.length > 0 ? parseInt(features_set[0], 10).toString(2).split('') : []),
163
+ ...(features_set.length > 1 ? parseInt(features_set[1], 10).toString(2).split('') : []),
164
+ ];
165
+ if (this.features.length > 0) {
166
+ this.transient = (this.features[this.features.length - 1 - 48] == '1');
167
+ }
168
+ if (this.statusflags.length) {
169
+ let PasswordRequired = (this.statusflags[this.statusflags.length - 1 - 7] == '1');
170
+ let PinRequired = (this.statusflags[this.statusflags.length - 1 - 3] == '1');
171
+ let OneTimePairingRequired = (this.statusflags[this.statusflags.length - 1 - 9] == '1');
172
+ // console.debug('needPss', PasswordRequired, PinRequired, OneTimePairingRequired);
173
+ this.needPassword = PasswordRequired;
174
+ this.needPin = (PinRequired || OneTimePairingRequired);
175
+ this.transient = !(PasswordRequired || PinRequired || OneTimePairingRequired);
176
+ }
177
+ if (this.airplay2 && this.statusflags.length === 0 && !this.needPassword && !this.needPin) {
178
+ this.transient = true;
179
+ }
180
+ // console.debug('transient', this.transient);
181
+ // detect old shairports with broken text
182
+ let oldver1 = this.txt.filter((u) => String(u).startsWith('sm='));
183
+ let oldver2 = this.txt.filter((u) => String(u).startsWith('sv='));
184
+ if ((b[0] ?? '') === 'cn=0,1' && (a[0] ?? '') === 'et=0,1' && (oldver1[0] ?? '') === 'sm=false' && (oldver2[0] ?? '') === 'sv=false' && this.statusflags.length === 0) {
185
+ // console.debug('borked shairport found');
186
+ this.alacEncoding = true;
187
+ this.borkedshp = true;
188
+ }
189
+ let k = this.txt.filter((u) => String(u).startsWith('am='));
190
+ if ((k[0] ?? '').includes('AppleTV3,1') || (k[0] ?? '').includes('AirReceiver3,1') || (k[0] ?? '').includes('AirRecever3,1') || (k[0] ?? '').includes('Shairport')) {
191
+ this.alacEncoding = true;
192
+ this.airplay2 = false;
193
+ }
194
+ k = this.txt.filter((u) => String(u).startsWith('rmodel='));
195
+ if ((k[0] ?? '').includes('AppleTV3,1') || (k[0] ?? '').includes('AirReceiver3,1') || (k[0] ?? '').includes('AirRecever3,1') || (k[0] ?? '').includes('Shairport')) {
196
+ this.alacEncoding = true;
197
+ this.airplay2 = false;
198
+ }
199
+ let manufacturer = this.txt.filter((u) => String(u).startsWith('manufacturer='));
200
+ if ((manufacturer[0] ?? '').includes('Sonos')) {
201
+ this.mode = 2;
202
+ this.needPin = true;
203
+ }
204
+ // console.debug('needPin', this.needPin);
205
+ // console.debug('mode-atv', this.mode);
206
+ // console.debug('alacEncoding', this.alacEncoding);
207
+ try {
208
+ this.rtsp = new rtsp_1.default.Client(options.volume || 50, options.password || null, audioOut, {
209
+ mode: this.mode,
210
+ txt: this.txt,
211
+ alacEncoding: this.alacEncoding,
212
+ needPassword: this.needPassword,
213
+ airplay2: this.airplay2,
214
+ needPin: this.needPin,
215
+ debug: options.debug,
216
+ transient: this.transient,
217
+ borkedshp: this.borkedshp,
218
+ log: this.log,
219
+ });
220
+ }
221
+ catch (e) {
222
+ this.logLine?.('rtsp error', e);
223
+ }
224
+ this.audioCallback = null;
225
+ this.encoder = [];
226
+ this.credentials = null;
227
+ // this.func = `
228
+ // const {Worker, isMainThread, parentPort, workerData} = require('node:worker_threads');
229
+ // var { WebSocketServer } = require('ws');
230
+ // const wss = new WebSocketServer({ port: 8980 });
231
+ // wss.on('connection', function connection(ws) {
232
+ // ws.on('message', function message(data) {
233
+ // parentPort.postMessage({message: data});
234
+ // });
235
+ // parentPort.on("message", data => {
236
+ // console.log("ass");
237
+ // ws.send(data);
238
+ // });
239
+ // });`;
240
+ // this.worker = new Worker(func, {eval: true});
241
+ }
242
+ Object.setPrototypeOf(AirTunesDevice.prototype, node_events_1.EventEmitter.prototype);
243
+ AirTunesDevice.prototype.start = function () {
244
+ var self = this;
245
+ this.audioSocket = node_dgram_1.default.createSocket('udp4');
246
+ // Wait until timing and control ports are chosen. We need them in RTSP handshake.
247
+ this.udpServers.on('ports', function (err) {
248
+ if (err) {
249
+ self.logLine?.(err.code);
250
+ self.status = 'stopped';
251
+ self.emit('status', 'stopped');
252
+ self.logLine?.('port issues');
253
+ self.emit('error', 'udp_ports', err.code);
254
+ return;
255
+ }
256
+ self.doHandshake();
257
+ });
258
+ this.udpServers.bind(this.host);
259
+ };
260
+ AirTunesDevice.prototype.doHandshake = function () {
261
+ var self = this;
262
+ try {
263
+ if (this.rtsp == null) {
264
+ try {
265
+ this.rtsp = new rtsp_1.default.Client(this.options.volume || 30, this.options.password || null, this.audioOut, {
266
+ mode: this.mode,
267
+ txt: this.txt,
268
+ alacEncoding: this.alacEncoding,
269
+ needPassword: this.needPassword,
270
+ airplay2: this.airplay2,
271
+ needPin: this.needPin,
272
+ debug: true,
273
+ transient: this.transient,
274
+ borkedshp: this.borkedshp,
275
+ log: this.log,
276
+ });
277
+ }
278
+ catch (e) {
279
+ self.logLine?.(e);
280
+ }
281
+ }
282
+ if (!this.rtsp) {
283
+ return;
284
+ }
285
+ this.rtsp.on('config', function (setup) {
286
+ self.audioLatency = setup.audioLatency;
287
+ self.requireEncryption = setup.requireEncryption;
288
+ self.serverPort = setup.server_port;
289
+ self.controlPort = setup.control_port;
290
+ self.timingPort = setup.timing_port;
291
+ self.credentials = setup.credentials;
292
+ try {
293
+ self.audioOut?.setLatencyFrames?.(self.audioLatency);
294
+ }
295
+ catch {
296
+ /* ignore */
297
+ }
298
+ });
299
+ this.rtsp.on('ready', function () {
300
+ self.relayAudio();
301
+ });
302
+ this.rtsp.on('need_password', function () {
303
+ self.emit('status', 'need_password');
304
+ });
305
+ this.rtsp.on('pair_failed', function () {
306
+ self.emit('status', 'pair_failed');
307
+ });
308
+ this.rtsp.on('pair_success', function () {
309
+ self.emit('status', 'pair_success');
310
+ });
311
+ this.rtsp.on('end', function (err) {
312
+ self.logLine?.(err);
313
+ self.cleanup();
314
+ if (err !== 'stopped')
315
+ self.emit(err);
316
+ });
317
+ }
318
+ catch (e) {
319
+ self.logLine?.(e);
320
+ }
321
+ // console.log(this.udpServers, this.host,this.port)
322
+ if (!this.rtsp) {
323
+ return;
324
+ }
325
+ this.rtsp.startHandshake(this.udpServers, this.host, this.port);
326
+ };
327
+ AirTunesDevice.prototype.relayAudio = function () {
328
+ var self = this;
329
+ this.status = 'ready';
330
+ this.emit('status', 'ready');
331
+ this.audioCallback = function (packet) {
332
+ var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials, self.inputCodec);
333
+ // if (self.credentials) {
334
+ // airTunes = self.credentials.encrypt(airTunes)
335
+ // }
336
+ if (self.audioSocket == null) {
337
+ self.audioSocket = node_dgram_1.default.createSocket('udp4');
338
+ }
339
+ self.audioSocket.send(airTunes, 0, airTunes.length, self.serverPort, self.host);
340
+ };
341
+ // this.sendAirTunesPacket = function(airTunes) {
342
+ // try{
343
+ // if(self.audioSocket == null){
344
+ // self.audioSocket = dgram.createSocket('udp4');
345
+ // }
346
+ // self.audioSocket.send(
347
+ // airTunes, 0, airTunes.length,
348
+ // self.serverPort, self.host
349
+ // );} catch(e){
350
+ // console.log('send error',e)
351
+ // }
352
+ // };
353
+ // this.audioCallback = function(packet) {
354
+ // var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials);
355
+ // try{
356
+ // self.sendAirTunesPacket(airTunes);
357
+ // self.audioPacketHistory.add(packet.seq, airTunes); // If we need to resend it
358
+ // } catch(e){}
359
+ // };
360
+ // this.udpServers.on('resendRequested', function (missedSeq, count) {
361
+ // try{
362
+ // for (var i = 0; i < count; i++) {
363
+ // airTunes = self.audioPacketHistory.getLatestNamed(missedSeq + i);
364
+ // if (airTunes != null)
365
+ // self.sendAirTunesPacket(airTunes);}}
366
+ // catch (_){}
367
+ // });
368
+ this.audioOut.on('packet', this.audioCallback);
369
+ };
370
+ AirTunesDevice.prototype.onSyncNeeded = function (seq) {
371
+ this.udpServers.sendControlSync(seq, this);
372
+ //if ( this.airplay2)this.rtsp.sendControlSync(seq, this, this.rtsp);
373
+ };
374
+ AirTunesDevice.prototype.cleanup = function () {
375
+ this.audioSocket = null;
376
+ this.audioPacketHistory = null;
377
+ this.status = 'stopped';
378
+ this.emit('status', 'stopped');
379
+ // console.debug('stop');
380
+ if (this.audioCallback) {
381
+ this.audioOut.removeListener('packet', this.audioCallback);
382
+ this.audioCallback = null;
383
+ }
384
+ this.udpServers.close();
385
+ this.removeAllListeners();
386
+ this.rtsp = null;
387
+ };
388
+ AirTunesDevice.prototype.reportStatus = function () {
389
+ this.emit('status', this.status);
390
+ };
391
+ AirTunesDevice.prototype.stop = function (cb) {
392
+ try {
393
+ if (!this.rtsp)
394
+ return;
395
+ this.rtsp.once('end', function () {
396
+ if (cb)
397
+ cb();
398
+ });
399
+ // console.debug('teardown');
400
+ this.rtsp.teardown();
401
+ }
402
+ catch (_) { }
403
+ };
404
+ AirTunesDevice.prototype.setVolume = function (volume, callback) {
405
+ if (!this.rtsp)
406
+ return;
407
+ this.rtsp.setVolume(volume, callback);
408
+ };
409
+ AirTunesDevice.prototype.setTrackInfo = function (name, artist, album, callback) {
410
+ if (!this.rtsp)
411
+ return;
412
+ this.rtsp.setTrackInfo(name, artist, album, callback);
413
+ };
414
+ AirTunesDevice.prototype.setProgress = function (progress, duration, callback) {
415
+ if (!this.rtsp)
416
+ return;
417
+ this.rtsp.setProgress(progress, duration, callback);
418
+ };
419
+ AirTunesDevice.prototype.setArtwork = function (art, contentType, callback) {
420
+ if (!this.rtsp)
421
+ return;
422
+ this.rtsp.setArtwork(art, contentType, callback);
423
+ };
424
+ AirTunesDevice.prototype.setPasscode = function (password) {
425
+ if (!this.rtsp)
426
+ return;
427
+ this.rtsp.setPasscode(password);
428
+ };
429
+ AirTunesDevice.prototype.requireEncryption = function () {
430
+ return Boolean(this.requireEncryption);
431
+ };
432
+ exports.default = AirTunesDevice;
433
+ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm') {
434
+ const useAlacInput = inputCodec === 'alac';
435
+ var alac = useAlacInput
436
+ ? packet.pcm
437
+ : (alacEncoding || credentials)
438
+ ? (0, alac_1.encodePcmToAlac)(packet.pcm)
439
+ : pcmParse(packet.pcm);
440
+ var airTunes = Buffer.alloc(alac.length + RTP_HEADER_SIZE);
441
+ var header = makeRTPHeader(packet);
442
+ if (requireEncryption) {
443
+ alac = encryptAES(alac, alac.length);
444
+ }
445
+ if (credentials) {
446
+ let pcm = credentials.encryptAudio(alac, header.slice(4, 12), packet.seq);
447
+ let airplay = Buffer.alloc(RTP_HEADER_SIZE + pcm.length);
448
+ header.copy(airplay);
449
+ pcm.copy(airplay, RTP_HEADER_SIZE);
450
+ return airplay;
451
+ // console.log(alac.length)
452
+ }
453
+ else {
454
+ header.copy(airTunes);
455
+ alac.copy(airTunes, RTP_HEADER_SIZE);
456
+ return airTunes;
457
+ }
458
+ }
459
+ function pcmParse(pcmData) {
460
+ let dst = new Uint8Array(352 * 4);
461
+ let src = pcmData;
462
+ let a = 0;
463
+ let b = 0;
464
+ let size;
465
+ for (size = 0; size < 352; size++) {
466
+ dst[a++] = src[b + 1];
467
+ dst[a++] = src[b++];
468
+ b++;
469
+ dst[a++] = src[b + 1];
470
+ dst[a++] = src[b++];
471
+ b++;
472
+ }
473
+ return Buffer.from(dst);
474
+ }
475
+ function encryptAES(alacData, alacSize) {
476
+ let result = Buffer.concat([]);
477
+ const isv = Buffer.from([0x78, 0xf4, 0x41, 0x2c, 0x8d, 0x17, 0x37, 0x90, 0x2b, 0x15, 0xa6, 0xb3, 0xee, 0x77, 0x0d, 0x67]);
478
+ const aes_key = Buffer.from([0x14, 0x49, 0x7d, 0xcc, 0x98, 0xe1, 0x37, 0xa8, 0x55, 0xc1, 0x45, 0x5a, 0x6b, 0xc0, 0xc9, 0x79]);
479
+ let remainder = alacData.length % 16;
480
+ let end_of_encoded_data = alacData.length - remainder;
481
+ let cipher = node_crypto_1.default.createCipheriv('aes-128-cbc', aes_key, isv);
482
+ cipher.setAutoPadding(false);
483
+ let i = 0;
484
+ let l = end_of_encoded_data - 16;
485
+ for (i = 0, l = end_of_encoded_data - 16; i <= l; i += 16) {
486
+ let chunk = cipher.update(alacData.slice(i, i + 16));
487
+ result = Buffer.concat([result, chunk]);
488
+ }
489
+ return Buffer.concat([result, alacData.slice(end_of_encoded_data)]);
490
+ }
491
+ function makeRTPHeader(packet) {
492
+ var header = Buffer.alloc(RTP_HEADER_SIZE);
493
+ if (packet.seq === 0)
494
+ header.writeUInt16BE(0x80e0, 0);
495
+ else
496
+ header.writeUInt16BE(0x8060, 0);
497
+ header.writeUInt16BE(nu.low16(packet.seq), 2);
498
+ header.writeUInt32BE(packet.timestamp, 4);
499
+ header.writeUInt32BE(config_1.default.device_magic, 8);
500
+ return header;
501
+ }
@@ -0,0 +1,50 @@
1
+ import { EventEmitter } from 'node:events';
2
+ type DeviceStatus = 'connecting' | 'ready' | 'playing' | 'stopped' | 'error' | string;
3
+ type AirTunesDeviceInstance = any;
4
+ type DevicesEmitter = EventEmitter & {
5
+ emit(event: 'status', key: string, status: DeviceStatus, desc: string): boolean;
6
+ emit(event: 'airtunes_devices', hasAirTunes: boolean): boolean;
7
+ emit(event: 'need_sync'): boolean;
8
+ };
9
+ /**
10
+ * Tracks and controls all connected AirPlay/RAOP devices.
11
+ * Responsible for lifecycle, fan-out of state, and sync signaling.
12
+ */
13
+ export default class Devices extends EventEmitter implements DevicesEmitter {
14
+ private readonly audioOut;
15
+ private readonly devices;
16
+ private hasAirTunes;
17
+ constructor(audioOut: EventEmitter);
18
+ /** Wire device sync events from AudioOut into individual devices. */
19
+ init(): void;
20
+ /** Iterate over live devices. */
21
+ private forEach;
22
+ /**
23
+ * Add (or reuse) a device and start playback.
24
+ * @param type Device type (airtunes/coreaudio).
25
+ * @param host Target host/IP.
26
+ * @param options Transport options (volume/password/etc).
27
+ * @param mode RAOP mode (0 default, 2 for AirPlay 2).
28
+ * @param txt TXT records advertised by the device.
29
+ */
30
+ add(type: string, host: string | null, options: any, mode?: number, txt?: string[] | string): AirTunesDeviceInstance;
31
+ /** Adjust volume on one device. */
32
+ setVolume(key: string, volume: number, callback?: (err?: unknown) => void): void;
33
+ /** Push playback position to one or all devices. */
34
+ setProgress(key: string, progress: number, duration: number, callback?: (err?: unknown) => void): void;
35
+ /**
36
+ * Update track info on one or all devices.
37
+ */
38
+ setTrackInfo(key: string, name: string, artist?: string, album?: string, callback?: (err?: unknown) => void): void;
39
+ /** Update artwork on one or all devices. */
40
+ setArtwork(key: string, art: Buffer, contentType?: string, callback?: (err?: unknown) => void): void;
41
+ /** Provide a passcode to a specific device. */
42
+ setPasscode(key: string, passcode: string): void;
43
+ /** Stop one device. */
44
+ stop(key: string): void;
45
+ /** Stop every device in parallel. */
46
+ stopAll(allCb?: (err?: unknown) => void): void;
47
+ /** Track whether any active device is RAOP to drive sync signals. */
48
+ private checkAirTunesDevices;
49
+ }
50
+ export {};