@lox-audioserver/node-airplay-sender 0.4.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 +93 -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 +43 -0
  9. package/dist/core/audioOut.js +220 -0
  10. package/dist/core/deviceAirtunes.d.ts +76 -0
  11. package/dist/core/deviceAirtunes.js +536 -0
  12. package/dist/core/devices.d.ts +50 -0
  13. package/dist/core/devices.js +221 -0
  14. package/dist/core/index.d.ts +56 -0
  15. package/dist/core/index.js +144 -0
  16. package/dist/core/rtsp.d.ts +12 -0
  17. package/dist/core/rtsp.js +1678 -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 +44 -0
  21. package/dist/core/udpServers.js +244 -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 +220 -0
  26. package/dist/esm/core/deviceAirtunes.js +536 -0
  27. package/dist/esm/core/devices.js +221 -0
  28. package/dist/esm/core/index.js +144 -0
  29. package/dist/esm/core/rtsp.js +1678 -0
  30. package/dist/esm/core/srp.js +128 -0
  31. package/dist/esm/core/udpServers.js +244 -0
  32. package/dist/esm/homekit/credentials.js +109 -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 +310 -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 +132 -0
  41. package/dist/esm/utils/config.js +49 -0
  42. package/dist/esm/utils/http.js +148 -0
  43. package/dist/esm/utils/ntp.js +56 -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 +31 -0
  48. package/dist/homekit/credentials.js +109 -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 +121 -0
  56. package/dist/index.js +310 -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 +32 -0
  62. package/dist/utils/circularBuffer.js +132 -0
  63. package/dist/utils/config.d.ts +42 -0
  64. package/dist/utils/config.js +49 -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 +21 -0
  68. package/dist/utils/ntp.js +56 -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 +71 -0
@@ -0,0 +1,536 @@
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
+ const resendBufferSize = Number(options?.resendBufferSize ?? config_1.default.resend_buffer_size ?? 1000);
112
+ this.audioPacketHistory = new BufferWithNames(Number.isFinite(resendBufferSize) && resendBufferSize > 0 ? resendBufferSize : 1000);
113
+ this.udpServers = new udpServers_1.default();
114
+ this.audioOut = audioOut;
115
+ this.type = 'airtunes';
116
+ this.deviceMagic = Number(options?.deviceMagic ?? config_1.default.device_magic);
117
+ this.options = options;
118
+ this.log = options?.log;
119
+ this.logLine = (...args) => logLine(this, ...args);
120
+ this.host = host;
121
+ this.port = Number(options?.port ?? 5000);
122
+ this.key = this.host + ':' + this.port;
123
+ this.mode = mode; // Homepods with or without passcode
124
+ // if (options.password != null && compatMode === true) {
125
+ // this.mode = 1; // Airport / Shairport passcode mode
126
+ // this.mode = 2 // MFi mode
127
+ // }
128
+ this.forceAlac = options.forceAlac ?? true;
129
+ // this.skipAutoVolume = options.skipAutoVolume ?? false
130
+ this.statusflags = [];
131
+ this.alacEncoding = options?.alacEncoding ?? true;
132
+ this.inputCodec = options?.inputCodec ?? 'pcm';
133
+ this.airplay2 = options?.airplay2 ?? false;
134
+ this.txt = Array.isArray(txt) ? txt : txt ? [String(txt)] : [];
135
+ this.borkedshp = false;
136
+ if (this.airplay2 && this.mode === 0) {
137
+ this.mode = 2;
138
+ }
139
+ // console.debug('airplay txt', this.txt, 'port', this.port);
140
+ let a = this.txt.filter((u) => String(u).startsWith('et='));
141
+ if ((a[0] ?? '').includes('4')) {
142
+ this.mode = 2;
143
+ }
144
+ let b = this.txt.filter((u) => String(u).startsWith('cn='));
145
+ if (!this.forceAlac) {
146
+ if ((b[0] ?? '').includes('0')) {
147
+ this.alacEncoding = false;
148
+ }
149
+ }
150
+ let c = this.txt.filter((u) => String(u).startsWith('sf='));
151
+ this.statusflags = c[0] ? parseInt(c[0].substring(3)).toString(2).split('') : [];
152
+ if (c.length == 0) {
153
+ c = this.txt.filter((u) => String(u).startsWith('flags='));
154
+ this.statusflags = c[0] ? parseInt(c[0].substring(6)).toString(2).split('') : [];
155
+ }
156
+ this.needPassword = false;
157
+ this.needPin = false;
158
+ const transientOption = options?.transient;
159
+ this.transient = typeof transientOption === 'boolean' ? transientOption : false;
160
+ let d = this.txt.filter((u) => String(u).startsWith('features='));
161
+ if (d.length === 0)
162
+ d = this.txt.filter((u) => String(u).startsWith('ft='));
163
+ const features_set = d.length > 0 ? d[0].substring(d[0].indexOf('=') + 1).split(',') : [];
164
+ this.features = [
165
+ ...(features_set.length > 0 ? parseInt(features_set[0], 10).toString(2).split('') : []),
166
+ ...(features_set.length > 1 ? parseInt(features_set[1], 10).toString(2).split('') : []),
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
+ }
176
+ // console.debug('transient', this.transient);
177
+ // detect old shairports with broken text
178
+ let oldver1 = this.txt.filter((u) => String(u).startsWith('sm='));
179
+ let oldver2 = this.txt.filter((u) => String(u).startsWith('sv='));
180
+ if ((b[0] ?? '') === 'cn=0,1' && (a[0] ?? '') === 'et=0,1' && (oldver1[0] ?? '') === 'sm=false' && (oldver2[0] ?? '') === 'sv=false' && this.statusflags.length === 0) {
181
+ // console.debug('borked shairport found');
182
+ this.alacEncoding = true;
183
+ this.borkedshp = true;
184
+ }
185
+ let k = this.txt.filter((u) => String(u).startsWith('am='));
186
+ if ((k[0] ?? '').includes('AppleTV3,1') || (k[0] ?? '').includes('AirReceiver3,1') || (k[0] ?? '').includes('AirRecever3,1') || (k[0] ?? '').includes('Shairport')) {
187
+ this.alacEncoding = true;
188
+ this.airplay2 = false;
189
+ }
190
+ k = this.txt.filter((u) => String(u).startsWith('rmodel='));
191
+ if ((k[0] ?? '').includes('AppleTV3,1') || (k[0] ?? '').includes('AirReceiver3,1') || (k[0] ?? '').includes('AirRecever3,1') || (k[0] ?? '').includes('Shairport')) {
192
+ this.alacEncoding = true;
193
+ this.airplay2 = false;
194
+ }
195
+ let manufacturer = this.txt.filter((u) => String(u).startsWith('manufacturer='));
196
+ if ((manufacturer[0] ?? '').includes('Sonos')) {
197
+ this.mode = 2;
198
+ this.needPin = true;
199
+ }
200
+ // console.debug('needPin', this.needPin);
201
+ // console.debug('mode-atv', this.mode);
202
+ // console.debug('alacEncoding', this.alacEncoding);
203
+ try {
204
+ this.rtsp = new rtsp_1.default.Client(options.volume || 50, options.password || null, audioOut, {
205
+ mode: this.mode,
206
+ txt: this.txt,
207
+ alacEncoding: this.alacEncoding,
208
+ needPassword: this.needPassword,
209
+ airplay2: this.airplay2,
210
+ needPin: this.needPin,
211
+ debug: options.debug,
212
+ transient: this.transient,
213
+ borkedshp: this.borkedshp,
214
+ log: this.log,
215
+ });
216
+ }
217
+ catch (e) {
218
+ this.logLine?.('rtsp error', e);
219
+ }
220
+ this.audioCallback = null;
221
+ this.encoder = [];
222
+ this.credentials = null;
223
+ this.audioNonce = Math.floor(Math.random() * 0xffffffff);
224
+ this.onUnderrun = () => {
225
+ this.encoder = [];
226
+ this.logLine?.('underrun_encoder_reset');
227
+ this.audioNonce = Math.floor(Math.random() * 0xffffffff);
228
+ if (this.credentials) {
229
+ this.credentials.encryptCount = 0;
230
+ this.credentials.decryptCount = 0;
231
+ }
232
+ };
233
+ this.packetsSent = 0;
234
+ // this.func = `
235
+ // const {Worker, isMainThread, parentPort, workerData} = require('node:worker_threads');
236
+ // var { WebSocketServer } = require('ws');
237
+ // const wss = new WebSocketServer({ port: 8980 });
238
+ // wss.on('connection', function connection(ws) {
239
+ // ws.on('message', function message(data) {
240
+ // parentPort.postMessage({message: data});
241
+ // });
242
+ // parentPort.on("message", data => {
243
+ // console.log("ass");
244
+ // ws.send(data);
245
+ // });
246
+ // });`;
247
+ // this.worker = new Worker(func, {eval: true});
248
+ }
249
+ Object.setPrototypeOf(AirTunesDevice.prototype, node_events_1.EventEmitter.prototype);
250
+ AirTunesDevice.prototype.start = function () {
251
+ this.audioSocket = node_dgram_1.default.createSocket('udp4');
252
+ // Wait until timing and control ports are chosen. We need them in RTSP handshake.
253
+ this.udpServers.on('ports', (err) => {
254
+ if (err) {
255
+ this.logLine?.(err.code);
256
+ this.status = 'stopped';
257
+ this.emit('status', 'stopped');
258
+ this.logLine?.('port issues');
259
+ this.emit('error', 'udp_ports', err.code);
260
+ return;
261
+ }
262
+ this.doHandshake();
263
+ });
264
+ this.udpServers.bind(this.host);
265
+ };
266
+ AirTunesDevice.prototype.doHandshake = function () {
267
+ try {
268
+ if (this.rtsp == null) {
269
+ try {
270
+ this.rtsp = new rtsp_1.default.Client(this.options.volume || 30, this.options.password || null, this.audioOut, {
271
+ mode: this.mode,
272
+ txt: this.txt,
273
+ alacEncoding: this.alacEncoding,
274
+ needPassword: this.needPassword,
275
+ airplay2: this.airplay2,
276
+ needPin: this.needPin,
277
+ debug: true,
278
+ transient: this.transient,
279
+ borkedshp: this.borkedshp,
280
+ log: this.log,
281
+ });
282
+ }
283
+ catch (e) {
284
+ this.logLine?.(e);
285
+ }
286
+ }
287
+ if (!this.rtsp) {
288
+ return;
289
+ }
290
+ this.rtsp.on('config', (setup) => {
291
+ this.audioLatency = setup.audioLatency;
292
+ this.requireEncryption = setup.requireEncryption;
293
+ this.serverPort = setup.server_port;
294
+ this.controlPort = setup.control_port;
295
+ this.timingPort = setup.timing_port;
296
+ this.credentials = setup.credentials;
297
+ try {
298
+ this.audioOut?.setLatencyFrames?.(this.audioLatency);
299
+ }
300
+ catch {
301
+ /* ignore */
302
+ }
303
+ });
304
+ this.rtsp.on('ready', () => {
305
+ this.status = 'playing';
306
+ this.emit('status', 'playing');
307
+ this.relayAudio();
308
+ });
309
+ this.rtsp.on('need_password', () => {
310
+ this.emit('status', 'need_password');
311
+ });
312
+ this.rtsp.on('pair_failed', () => {
313
+ this.emit('status', 'pair_failed');
314
+ });
315
+ this.rtsp.on('pair_success', () => {
316
+ this.emit('status', 'pair_success');
317
+ });
318
+ this.rtsp.on('end', (err) => {
319
+ this.logLine?.(err);
320
+ this.cleanup();
321
+ if (err !== 'stopped')
322
+ this.emit(err);
323
+ });
324
+ }
325
+ catch (e) {
326
+ this.logLine?.(e);
327
+ }
328
+ // console.log(this.udpServers, this.host,this.port)
329
+ if (!this.rtsp) {
330
+ return;
331
+ }
332
+ this.rtsp.startHandshake(this.udpServers, this.host, this.port);
333
+ };
334
+ AirTunesDevice.prototype.relayAudio = function () {
335
+ if (this.audioCallback) {
336
+ return;
337
+ }
338
+ this.audioCallback = (packet) => {
339
+ const airTunes = makeAirTunesPacket(packet, this.encoder, this.requireEncryption, this.alacEncoding, this.credentials, this.inputCodec, this.deviceMagic, this.audioNonce);
340
+ this.packetsSent += 1;
341
+ this.audioNonce = (this.audioNonce + 1) >>> 0;
342
+ try {
343
+ this.audioPacketHistory?.add(packet.seq, airTunes);
344
+ }
345
+ catch {
346
+ /* ignore history errors */
347
+ }
348
+ // if (self.credentials) {
349
+ // airTunes = self.credentials.encrypt(airTunes)
350
+ // }
351
+ if (this.audioSocket == null) {
352
+ this.audioSocket = node_dgram_1.default.createSocket('udp4');
353
+ }
354
+ this.audioSocket.send(airTunes, 0, airTunes.length, this.serverPort, this.host);
355
+ };
356
+ // this.sendAirTunesPacket = function(airTunes) {
357
+ // try{
358
+ // if(self.audioSocket == null){
359
+ // self.audioSocket = dgram.createSocket('udp4');
360
+ // }
361
+ // self.audioSocket.send(
362
+ // airTunes, 0, airTunes.length,
363
+ // self.serverPort, self.host
364
+ // );} catch(e){
365
+ // console.log('send error',e)
366
+ // }
367
+ // };
368
+ // this.audioCallback = function(packet) {
369
+ // var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials);
370
+ // try{
371
+ // self.sendAirTunesPacket(airTunes);
372
+ // self.audioPacketHistory.add(packet.seq, airTunes); // If we need to resend it
373
+ // } catch(e){}
374
+ // };
375
+ // this.udpServers.on('resendRequested', function (missedSeq, count) {
376
+ // try{
377
+ // for (var i = 0; i < count; i++) {
378
+ // airTunes = self.audioPacketHistory.getLatestNamed(missedSeq + i);
379
+ // if (airTunes != null)
380
+ // self.sendAirTunesPacket(airTunes);}}
381
+ // catch (_){}
382
+ // });
383
+ this.udpServers.on('resendRequested', (missedSeq, count) => {
384
+ for (let i = 0; i < count; i += 1) {
385
+ const seq = missedSeq + i;
386
+ try {
387
+ const pkt = this.audioPacketHistory?.getLatestNamed(seq);
388
+ if (pkt && this.audioSocket) {
389
+ this.audioSocket.send(pkt, 0, pkt.length, this.serverPort, this.host);
390
+ }
391
+ }
392
+ catch {
393
+ // ignore resend errors
394
+ }
395
+ }
396
+ });
397
+ this.audioOut.on('packet', this.audioCallback);
398
+ };
399
+ AirTunesDevice.prototype.onSyncNeeded = function (seq, tsOffsetFrames = 0, rtcp) {
400
+ this.udpServers.sendControlSync(seq, this, tsOffsetFrames, rtcp, { ssrc: this.deviceMagic }, { ntp: rtcp?.xr?.ntp, ssrc: this.deviceMagic });
401
+ //if ( this.airplay2)this.rtsp.sendControlSync(seq, this, this.rtsp);
402
+ };
403
+ AirTunesDevice.prototype.cleanup = function () {
404
+ this.audioSocket = null;
405
+ this.audioPacketHistory = null;
406
+ this.status = 'stopped';
407
+ this.emit('status', 'stopped');
408
+ // console.debug('stop');
409
+ if (this.audioCallback) {
410
+ this.audioOut.removeListener('packet', this.audioCallback);
411
+ this.audioCallback = null;
412
+ }
413
+ this.encoder = [];
414
+ this.audioNonce = Math.floor(Math.random() * 0xffffffff);
415
+ if (this.credentials) {
416
+ this.credentials.rotateKeys?.();
417
+ }
418
+ this.packetsSent = 0;
419
+ this.udpServers.close();
420
+ this.removeAllListeners();
421
+ this.rtsp = null;
422
+ };
423
+ AirTunesDevice.prototype.reportStatus = function () {
424
+ this.emit('status', this.status);
425
+ };
426
+ AirTunesDevice.prototype.stop = function (cb) {
427
+ try {
428
+ if (!this.rtsp) {
429
+ if (cb)
430
+ cb();
431
+ return;
432
+ }
433
+ this.rtsp.once('end', function () {
434
+ if (cb)
435
+ cb();
436
+ });
437
+ // console.debug('teardown');
438
+ this.rtsp.teardown();
439
+ }
440
+ catch (_) { }
441
+ };
442
+ AirTunesDevice.prototype.setVolume = function (volume, callback) {
443
+ if (!this.rtsp)
444
+ return;
445
+ this.rtsp.setVolume(volume, callback);
446
+ };
447
+ AirTunesDevice.prototype.setTrackInfo = function (name, artist, album, callback) {
448
+ if (!this.rtsp)
449
+ return;
450
+ this.rtsp.setTrackInfo(name, artist, album, callback);
451
+ };
452
+ AirTunesDevice.prototype.setProgress = function (progress, duration, callback) {
453
+ if (!this.rtsp)
454
+ return;
455
+ this.rtsp.setProgress(progress, duration, callback);
456
+ };
457
+ AirTunesDevice.prototype.setArtwork = function (art, contentType, callback) {
458
+ if (!this.rtsp)
459
+ return;
460
+ this.rtsp.setArtwork(art, contentType, callback);
461
+ };
462
+ AirTunesDevice.prototype.setPasscode = function (password) {
463
+ if (!this.rtsp)
464
+ return;
465
+ this.rtsp.setPasscode(password);
466
+ };
467
+ exports.default = AirTunesDevice;
468
+ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, inputCodec = 'pcm', deviceMagic = config_1.default.device_magic, audioNonce = packet.seq) {
469
+ const useAlacInput = inputCodec === 'alac';
470
+ var alac = useAlacInput
471
+ ? packet.pcm
472
+ : (alacEncoding || credentials)
473
+ ? (0, alac_1.encodePcmToAlac)(packet.pcm)
474
+ : pcmParse(packet.pcm);
475
+ var airTunes = Buffer.alloc(alac.length + RTP_HEADER_SIZE);
476
+ var header = makeRTPHeader(packet, deviceMagic);
477
+ if (requireEncryption) {
478
+ alac = encryptAES(alac, alac.length);
479
+ }
480
+ if (credentials) {
481
+ let pcm = credentials.encryptAudio(alac, header.slice(4, 12), audioNonce);
482
+ let airplay = Buffer.alloc(RTP_HEADER_SIZE + pcm.length);
483
+ header.copy(airplay);
484
+ pcm.copy(airplay, RTP_HEADER_SIZE);
485
+ return airplay;
486
+ // console.log(alac.length)
487
+ }
488
+ else {
489
+ header.copy(airTunes);
490
+ alac.copy(airTunes, RTP_HEADER_SIZE);
491
+ return airTunes;
492
+ }
493
+ }
494
+ function pcmParse(pcmData) {
495
+ let dst = new Uint8Array(352 * 4);
496
+ let src = pcmData;
497
+ let a = 0;
498
+ let b = 0;
499
+ let size;
500
+ for (size = 0; size < 352; size++) {
501
+ dst[a++] = src[b + 1];
502
+ dst[a++] = src[b++];
503
+ b++;
504
+ dst[a++] = src[b + 1];
505
+ dst[a++] = src[b++];
506
+ b++;
507
+ }
508
+ return Buffer.from(dst);
509
+ }
510
+ function encryptAES(alacData, alacSize) {
511
+ let result = Buffer.concat([]);
512
+ const isv = Buffer.from([0x78, 0xf4, 0x41, 0x2c, 0x8d, 0x17, 0x37, 0x90, 0x2b, 0x15, 0xa6, 0xb3, 0xee, 0x77, 0x0d, 0x67]);
513
+ const aes_key = Buffer.from([0x14, 0x49, 0x7d, 0xcc, 0x98, 0xe1, 0x37, 0xa8, 0x55, 0xc1, 0x45, 0x5a, 0x6b, 0xc0, 0xc9, 0x79]);
514
+ let remainder = alacData.length % 16;
515
+ let end_of_encoded_data = alacData.length - remainder;
516
+ let cipher = node_crypto_1.default.createCipheriv('aes-128-cbc', aes_key, isv);
517
+ cipher.setAutoPadding(false);
518
+ let i = 0;
519
+ let l = end_of_encoded_data - 16;
520
+ for (i = 0, l = end_of_encoded_data - 16; i <= l; i += 16) {
521
+ let chunk = cipher.update(alacData.slice(i, i + 16));
522
+ result = Buffer.concat([result, chunk]);
523
+ }
524
+ return Buffer.concat([result, alacData.slice(end_of_encoded_data)]);
525
+ }
526
+ function makeRTPHeader(packet, deviceMagic) {
527
+ var header = Buffer.alloc(RTP_HEADER_SIZE);
528
+ if (packet.seq === 0)
529
+ header.writeUInt16BE(0x80e0, 0);
530
+ else
531
+ header.writeUInt16BE(0x8060, 0);
532
+ header.writeUInt16BE(nu.low16(packet.seq), 2);
533
+ header.writeUInt32BE(packet.timestamp, 4);
534
+ header.writeUInt32BE(deviceMagic, 8);
535
+ return header;
536
+ }