@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,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Type Length Value encoding/decoding, used by HAP as a wire format.
5
+ * https://en.wikipedia.org/wiki/Type-length-value
6
+ */
7
+ const Tag = {
8
+ PairingMethod: 0x00,
9
+ Username: 0x01,
10
+ Salt: 0x02,
11
+ // could be either the SRP client public key (384 bytes) or the ED25519 public key (32 bytes), depending on context
12
+ PublicKey: 0x03,
13
+ Proof: 0x04,
14
+ EncryptedData: 0x05,
15
+ Sequence: 0x06,
16
+ ErrorCode: 0x07,
17
+ BackOff: 0x08,
18
+ Signature: 0x0a,
19
+ MFiCertificate: 0x09,
20
+ MFiSignature: 0x0a,
21
+ Flags: 0x13,
22
+ };
23
+ function encodeOne(type, data) {
24
+ let bufferData;
25
+ if (typeof data === 'number') {
26
+ bufferData = Buffer.from([data]);
27
+ }
28
+ else if (typeof data === 'string') {
29
+ bufferData = Buffer.from(data);
30
+ }
31
+ else {
32
+ bufferData = data;
33
+ }
34
+ if (bufferData.length <= 255) {
35
+ return Buffer.concat([Buffer.from([type, bufferData.length]), bufferData]);
36
+ }
37
+ let leftLength = bufferData.length;
38
+ let tempBuffer = Buffer.alloc(0);
39
+ let currentStart = 0;
40
+ for (; leftLength > 0;) {
41
+ if (leftLength >= 255) {
42
+ tempBuffer = Buffer.concat([
43
+ tempBuffer,
44
+ Buffer.from([type, 0xff]),
45
+ bufferData.slice(currentStart, currentStart + 255),
46
+ ]);
47
+ leftLength -= 255;
48
+ currentStart += 255;
49
+ }
50
+ else {
51
+ tempBuffer = Buffer.concat([
52
+ tempBuffer,
53
+ Buffer.from([type, leftLength]),
54
+ bufferData.slice(currentStart, currentStart + leftLength),
55
+ ]);
56
+ leftLength = 0;
57
+ }
58
+ }
59
+ return tempBuffer;
60
+ }
61
+ function encode(type, data, ...args) {
62
+ const encodedTLVBuffer = encodeOne(type, data);
63
+ if (args.length === 0) {
64
+ return encodedTLVBuffer;
65
+ }
66
+ const nextType = args[0];
67
+ const nextData = args[1];
68
+ const remaining = args.slice(2);
69
+ const remainingTLVBuffer = encode(nextType, nextData, ...remaining);
70
+ return Buffer.concat([encodedTLVBuffer, remainingTLVBuffer]);
71
+ }
72
+ function decode(data) {
73
+ const objects = {};
74
+ let leftLength = data.length;
75
+ let currentIndex = 0;
76
+ for (; leftLength > 0;) {
77
+ const type = data[currentIndex];
78
+ const length = data[currentIndex + 1];
79
+ currentIndex += 2;
80
+ leftLength -= 2;
81
+ const newData = data.slice(currentIndex, currentIndex + length);
82
+ if (objects[type]) {
83
+ objects[type] = Buffer.concat([objects[type], newData]);
84
+ }
85
+ else {
86
+ objects[type] = newData;
87
+ }
88
+ currentIndex += length;
89
+ leftLength -= length;
90
+ }
91
+ return objects;
92
+ }
93
+ exports.default = {
94
+ Tag,
95
+ encode,
96
+ decode,
97
+ };
@@ -0,0 +1,310 @@
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
+ exports.LoxAirplaySender = void 0;
40
+ exports.start = start;
41
+ const node_events_1 = require("node:events");
42
+ // Airtunes implementation (node_airtunes2 port) in src/core.
43
+ const index_1 = __importDefault(require("./core/index"));
44
+ const config_1 = __importStar(require("./utils/config"));
45
+ const ntp_1 = require("./utils/ntp");
46
+ class LoxAirplaySender extends node_events_1.EventEmitter {
47
+ airtunes = null;
48
+ deviceKey = null;
49
+ started = false;
50
+ source = null;
51
+ log;
52
+ lastTrackKey = null;
53
+ lastCoverKey = null;
54
+ lastProgressKey = null;
55
+ lastCoverUrl = null;
56
+ coverFetch;
57
+ artworkTimer;
58
+ pendingArtwork;
59
+ lastTrackChangeAt = 0;
60
+ /**
61
+ * Create + start a sender for a single AirPlay device.
62
+ * Returns true when the pipeline initializes; safe to call multiple times to restart.
63
+ */
64
+ start(options, onEvent) {
65
+ if (this.started) {
66
+ this.stop();
67
+ }
68
+ this.log = options.log;
69
+ if (options.config) {
70
+ (0, config_1.applyConfig)(options.config);
71
+ }
72
+ const inputCodec = options.inputCodec ?? 'pcm';
73
+ config_1.default.packet_size = inputCodec === 'alac' ? config_1.default.alac_packet_size : config_1.default.pcm_packet_size;
74
+ this.airtunes = new index_1.default({
75
+ packetSize: config_1.default.packet_size,
76
+ startTimeMs: options.startTimeMs,
77
+ startTimeNtp: options.startTimeNtp ? (0, ntp_1.toNtpTimestamp)(options.startTimeNtp) : undefined,
78
+ deviceMagic: options.deviceMagic,
79
+ resendBufferSize: options.resendBufferSize,
80
+ underrunMuteMs: options.underrunMuteMs,
81
+ config: options.config,
82
+ });
83
+ this.airtunes.on('device', (key, status, desc) => {
84
+ onEvent?.({ event: 'device', message: status, detail: { key, desc } });
85
+ });
86
+ this.airtunes.on('buffer', (status) => {
87
+ onEvent?.({ event: 'buffer', message: status });
88
+ });
89
+ this.airtunes.on('error', (err) => {
90
+ onEvent?.({ event: 'error', message: err instanceof Error ? err.message : String(err) });
91
+ });
92
+ this.airtunes.on('metrics', (detail) => {
93
+ onEvent?.({ event: 'metrics', detail });
94
+ });
95
+ const dev = this.airtunes.add(options.host, {
96
+ port: options.port,
97
+ name: options.name,
98
+ password: options.password ?? null,
99
+ volume: options.volume ?? 50,
100
+ mode: options.mode ?? (options.airplay2 ? 2 : 0),
101
+ txt: options.txt ?? [],
102
+ forceAlac: options.forceAlac ?? true,
103
+ alacEncoding: options.alacEncoding ?? true,
104
+ inputCodec,
105
+ airplay2: options.airplay2 ?? false,
106
+ debug: options.debug ?? false,
107
+ log: options.log,
108
+ });
109
+ this.deviceKey = dev?.key ?? `${options.host}:${options.port ?? 5000}`;
110
+ this.started = true;
111
+ return true;
112
+ }
113
+ /**
114
+ * Push raw PCM or ALAC frames into the stream.
115
+ */
116
+ sendPcm(chunk) {
117
+ if (!this.airtunes || !this.started)
118
+ return;
119
+ this.airtunes.write(chunk);
120
+ }
121
+ /**
122
+ * Pipe a readable stream into the sender; auto-stops on end/error.
123
+ */
124
+ pipeStream(stream) {
125
+ if (!this.airtunes || !this.started)
126
+ return;
127
+ this.source = stream;
128
+ stream.on('data', (chunk) => this.sendPcm(chunk));
129
+ stream.on('end', () => this.stop());
130
+ stream.on('error', () => this.stop());
131
+ }
132
+ /** Adjust receiver volume (0–100). */
133
+ setVolume(volume) {
134
+ if (!this.airtunes || !this.deviceKey)
135
+ return;
136
+ this.airtunes.setVolume(this.deviceKey, volume);
137
+ }
138
+ /** Update track metadata immediately without artwork/progress. */
139
+ setTrackInfo(title, artist, album) {
140
+ if (!this.airtunes || !this.deviceKey)
141
+ return;
142
+ this.airtunes.setTrackInfo(this.deviceKey, title, artist, album);
143
+ }
144
+ /** Send cover art immediately. */
145
+ setArtwork(art, contentType) {
146
+ if (!this.airtunes || !this.deviceKey)
147
+ return;
148
+ this.airtunes.setArtwork(this.deviceKey, art, contentType);
149
+ }
150
+ /** Send playback progress in seconds (elapsed, duration). */
151
+ setProgress(progress, duration) {
152
+ if (!this.airtunes || !this.deviceKey)
153
+ return;
154
+ this.airtunes.setProgress(this.deviceKey, progress, duration);
155
+ }
156
+ /**
157
+ * Convenience to send track info, cover (buffer or URL), and progress.
158
+ * Deduplicates payloads and staggers artwork on track changes.
159
+ */
160
+ async setMetadata(payload) {
161
+ if (!this.airtunes || !this.deviceKey)
162
+ return;
163
+ const title = payload.title ?? '';
164
+ const artist = payload.artist ?? '';
165
+ const album = payload.album ?? '';
166
+ const trackKey = `${title}::${artist}::${album}`;
167
+ const trackChanged = Boolean(title && trackKey !== this.lastTrackKey);
168
+ if (trackChanged) {
169
+ this.setTrackInfo(title, artist, album);
170
+ this.lastTrackKey = trackKey;
171
+ this.lastCoverKey = null;
172
+ this.lastCoverUrl = null;
173
+ this.lastTrackChangeAt = Date.now();
174
+ }
175
+ let coverPayload = payload.cover;
176
+ const coverUrl = payload.coverUrl;
177
+ if (!coverPayload?.data && coverUrl) {
178
+ if (coverUrl !== this.lastCoverUrl && !this.coverFetch) {
179
+ this.lastCoverUrl = coverUrl;
180
+ this.lastCoverKey = null;
181
+ this.coverFetch = this.fetchCover(coverUrl).finally(() => {
182
+ this.coverFetch = undefined;
183
+ });
184
+ }
185
+ if (this.coverFetch) {
186
+ coverPayload = (await this.coverFetch) ?? undefined;
187
+ }
188
+ }
189
+ if (coverPayload?.data) {
190
+ const coverKey = coverUrl
191
+ ? `${coverUrl}:${coverPayload.mime ?? 'unknown'}:${coverPayload.data.length}`
192
+ : `${coverPayload.mime ?? 'unknown'}:${coverPayload.data.length}`;
193
+ if (coverKey !== this.lastCoverKey) {
194
+ if (trackChanged) {
195
+ this.queueArtwork(coverPayload, coverKey, 200);
196
+ }
197
+ else {
198
+ this.sendArtworkNow(coverPayload, coverKey);
199
+ }
200
+ }
201
+ }
202
+ const durationInput = typeof payload.durationMs === 'number' && payload.durationMs > 0 ? payload.durationMs : null;
203
+ const elapsedInput = typeof payload.elapsedMs === 'number' && payload.elapsedMs >= 0 ? payload.elapsedMs : null;
204
+ const durationSec = durationInput !== null ? Math.floor(durationInput > 1000 ? durationInput / 1000 : durationInput) : null;
205
+ const elapsedSecRaw = elapsedInput !== null ? Math.floor(elapsedInput > 1000 ? elapsedInput / 1000 : elapsedInput) : null;
206
+ if (durationSec !== null && elapsedSecRaw !== null && durationSec > 0) {
207
+ const elapsedSec = Math.min(Math.max(0, elapsedSecRaw), durationSec);
208
+ const progressKey = `${elapsedSec}/${durationSec}`;
209
+ if (progressKey !== this.lastProgressKey) {
210
+ this.setProgress(elapsedSec, durationSec);
211
+ this.lastProgressKey = progressKey;
212
+ }
213
+ }
214
+ }
215
+ /** Provide a passcode when a receiver requests it. */
216
+ setPasscode(passcode) {
217
+ if (!this.airtunes || !this.deviceKey)
218
+ return;
219
+ this.airtunes.setPasscode(this.deviceKey, passcode);
220
+ }
221
+ /**
222
+ * Stop streaming and tear down state/sockets. Safe to call multiple times.
223
+ */
224
+ stop() {
225
+ if (this.source) {
226
+ try {
227
+ this.source.destroy();
228
+ }
229
+ catch {
230
+ // ignore
231
+ }
232
+ this.source = null;
233
+ }
234
+ this.lastTrackKey = null;
235
+ this.lastCoverKey = null;
236
+ this.lastProgressKey = null;
237
+ this.lastCoverUrl = null;
238
+ this.lastTrackChangeAt = 0;
239
+ this.pendingArtwork = undefined;
240
+ if (this.artworkTimer) {
241
+ clearTimeout(this.artworkTimer);
242
+ this.artworkTimer = undefined;
243
+ }
244
+ if (this.airtunes) {
245
+ if (this.deviceKey) {
246
+ this.airtunes.stop(this.deviceKey);
247
+ }
248
+ this.airtunes.stopAll?.(() => undefined);
249
+ this.airtunes.end?.();
250
+ }
251
+ this.airtunes = null;
252
+ this.deviceKey = null;
253
+ this.started = false;
254
+ }
255
+ async fetchCover(url) {
256
+ const controller = new AbortController();
257
+ const timeout = setTimeout(() => controller.abort(), 4000);
258
+ try {
259
+ const response = await fetch(url, { signal: controller.signal });
260
+ if (!response.ok) {
261
+ this.log?.('warn', 'airplay cover fetch failed', { status: response.status, url });
262
+ return null;
263
+ }
264
+ const mime = response.headers.get('content-type') || undefined;
265
+ const buffer = Buffer.from(await response.arrayBuffer());
266
+ if (!buffer.length) {
267
+ this.log?.('warn', 'airplay cover fetch empty', { url });
268
+ return null;
269
+ }
270
+ return { data: buffer, mime };
271
+ }
272
+ catch {
273
+ this.log?.('warn', 'airplay cover fetch error', { url });
274
+ return null;
275
+ }
276
+ finally {
277
+ clearTimeout(timeout);
278
+ }
279
+ }
280
+ sendArtworkNow(payload, coverKey) {
281
+ this.setArtwork(payload.data, payload.mime);
282
+ this.lastCoverKey = coverKey;
283
+ }
284
+ queueArtwork(payload, coverKey, delayMs) {
285
+ this.pendingArtwork = { ...payload, key: coverKey };
286
+ if (this.artworkTimer) {
287
+ clearTimeout(this.artworkTimer);
288
+ }
289
+ this.artworkTimer = setTimeout(() => {
290
+ this.artworkTimer = undefined;
291
+ const pending = this.pendingArtwork;
292
+ this.pendingArtwork = undefined;
293
+ if (!pending)
294
+ return;
295
+ if (pending.key === this.lastCoverKey)
296
+ return;
297
+ this.sendArtworkNow({ data: pending.data, mime: pending.mime }, pending.key);
298
+ }, delayMs);
299
+ }
300
+ }
301
+ exports.LoxAirplaySender = LoxAirplaySender;
302
+ /**
303
+ * Convenience helper to construct + start a sender in one call.
304
+ */
305
+ function start(options, onEvent) {
306
+ const sender = new LoxAirplaySender();
307
+ sender.start(options, onEvent);
308
+ return sender;
309
+ }
310
+ exports.default = LoxAirplaySender;
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ALAC_PACKET_SIZE = exports.PCM_PACKET_SIZE = void 0;
7
+ exports.encodePcmToAlac = encodePcmToAlac;
8
+ const config_1 = __importDefault(require("./config"));
9
+ /** PCM packet size (bytes) expected by the encoder. */
10
+ exports.PCM_PACKET_SIZE = config_1.default.pcm_packet_size;
11
+ /** Output ALAC packet size (bytes). */
12
+ exports.ALAC_PACKET_SIZE = config_1.default.alac_packet_size;
13
+ /**
14
+ * Encode one PCM frame (16-bit LE stereo, 44.1kHz) into ALAC.
15
+ * Input must be exactly `PCM_PACKET_SIZE` bytes.
16
+ */
17
+ function encodePcmToAlac(pcmData) {
18
+ let alacData = Buffer.alloc(exports.ALAC_PACKET_SIZE);
19
+ const bsize = 352;
20
+ const frames = 352;
21
+ const p = new Uint8Array(exports.ALAC_PACKET_SIZE);
22
+ const input = new Uint32Array(pcmData.length / 4);
23
+ let j = 0;
24
+ for (let i = 0; i < pcmData.length; i += 4) {
25
+ let res = pcmData[i];
26
+ res |= pcmData[i + 1] << 8;
27
+ res |= pcmData[i + 2] << 16;
28
+ res |= pcmData[i + 3] << 24;
29
+ input[j++] = res;
30
+ }
31
+ let pindex = 0;
32
+ let iindex = 0;
33
+ p[pindex++] = 1 << 5;
34
+ p[pindex++] = 0;
35
+ p[pindex++] = (1 << 4) | (1 << 1) | ((bsize & 0x80000000) >>> 31);
36
+ p[pindex++] = ((bsize & 0x7f800000) << 1) >>> 24;
37
+ p[pindex++] = ((bsize & 0x007f8000) << 1) >>> 16;
38
+ p[pindex++] = ((bsize & 0x00007f80) << 1) >>> 8;
39
+ p[pindex] = (bsize & 0x0000007f) << 1;
40
+ p[pindex++] |= (input[iindex] & 0x00008000) >>> 15;
41
+ let count = frames - 1;
42
+ while (count--) {
43
+ const i = input[iindex++];
44
+ p[pindex++] = (i & 0x00007f80) >>> 7;
45
+ p[pindex++] = ((i & 0x0000007f) << 1) | ((i & 0x80000000) >>> 31);
46
+ p[pindex++] = (i & 0x7f800000) >>> 23;
47
+ p[pindex++] = ((i & 0x007f0000) >>> 15) | ((input[iindex] & 0x00008000) >> 15);
48
+ }
49
+ const i = input[iindex];
50
+ p[pindex++] = (i & 0x00007f80) >>> 7;
51
+ p[pindex++] = ((i & 0x0000007f) << 1) | ((i & 0x80000000) >>> 31);
52
+ p[pindex++] = (i & 0x7f800000) >>> 23;
53
+ p[pindex++] = (i & 0x007f0000) >>> 15;
54
+ count = (bsize - frames) * 4;
55
+ while (count--)
56
+ p[pindex++] = 0;
57
+ p[pindex - 1] |= 1;
58
+ p[pindex++] = (7 >>> 1) << 6;
59
+ const alacSize = pindex;
60
+ alacData = Buffer.from(p.buffer);
61
+ return alacData.slice(0, alacSize);
62
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AlacEncoderStream = void 0;
4
+ const node_stream_1 = require("node:stream");
5
+ const alac_1 = require("./alac");
6
+ /**
7
+ * Transforms PCM (16-bit LE, stereo, 44.1kHz) into fixed-size ALAC frames.
8
+ * Emits ALAC packets sized per `ALAC_PACKET_SIZE`.
9
+ */
10
+ class AlacEncoderStream extends node_stream_1.Transform {
11
+ buffer = Buffer.alloc(0);
12
+ /** Create a streaming ALAC encoder for PCM input. */
13
+ constructor() {
14
+ super();
15
+ }
16
+ /**
17
+ * Buffer PCM until a full frame is available, then emit ALAC.
18
+ */
19
+ _transform(chunk, _encoding, callback) {
20
+ if (!chunk?.length) {
21
+ callback();
22
+ return;
23
+ }
24
+ this.buffer = (this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk);
25
+ while (this.buffer.length >= alac_1.PCM_PACKET_SIZE) {
26
+ const frame = this.buffer.subarray(0, alac_1.PCM_PACKET_SIZE);
27
+ this.buffer = this.buffer.subarray(alac_1.PCM_PACKET_SIZE);
28
+ const alac = (0, alac_1.encodePcmToAlac)(frame);
29
+ this.push(alac);
30
+ }
31
+ callback();
32
+ }
33
+ }
34
+ exports.AlacEncoderStream = AlacEncoderStream;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_events_1 = require("node:events");
7
+ const packetPool_1 = __importDefault(require("./packetPool"));
8
+ const WAITING = 0;
9
+ const FILLING = 1;
10
+ const NORMAL = 2;
11
+ const DRAINING = 3;
12
+ const ENDING = 4;
13
+ const ENDED = 5;
14
+ /**
15
+ * Fixed-size circular buffer that smooths incoming PCM/ALAC chunks into fixed packet sizes.
16
+ * Emits status changes for buffering/playing/drain/end to drive UI + sync.
17
+ */
18
+ class CircularBuffer extends node_events_1.EventEmitter {
19
+ packetPool;
20
+ maxSize;
21
+ packetSize;
22
+ writable = true;
23
+ muted = false;
24
+ buffers = [];
25
+ currentSize = 0;
26
+ status = WAITING;
27
+ hadUnderrun = false;
28
+ constructor(packetsInBuffer, packetSize) {
29
+ super();
30
+ this.packetPool = new packetPool_1.default(packetSize);
31
+ this.maxSize = packetsInBuffer * packetSize;
32
+ this.packetSize = packetSize;
33
+ }
34
+ /**
35
+ * Write a PCM/ALAC chunk into the buffer.
36
+ * Returns false when the buffer is full so upstream can throttle.
37
+ */
38
+ write(chunk) {
39
+ this.buffers.push(chunk);
40
+ this.currentSize += chunk.length;
41
+ if (this.status === ENDING || this.status === ENDED) {
42
+ throw new Error('Cannot write in buffer after closing it');
43
+ }
44
+ if (this.status === WAITING) {
45
+ this.emit('status', 'buffering');
46
+ this.status = FILLING;
47
+ }
48
+ if (this.status === FILLING && this.currentSize > this.maxSize / 2) {
49
+ this.status = NORMAL;
50
+ this.emit('status', 'playing');
51
+ }
52
+ if (this.currentSize >= this.maxSize) {
53
+ this.status = DRAINING;
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+ /**
59
+ * Read the next fixed-size packet, zero-filling gaps to preserve timing.
60
+ */
61
+ readPacket() {
62
+ const packet = this.packetPool.getPacket();
63
+ if (this.status !== ENDING &&
64
+ this.status !== ENDED &&
65
+ (this.status === FILLING || this.currentSize < this.packetSize)) {
66
+ packet.pcm.fill(0);
67
+ if (!this.hadUnderrun) {
68
+ this.hadUnderrun = true;
69
+ this.emit('underrun');
70
+ }
71
+ if (this.status !== FILLING && this.status !== WAITING) {
72
+ this.status = FILLING;
73
+ this.emit('status', 'buffering');
74
+ }
75
+ }
76
+ else {
77
+ let offset = 0;
78
+ let remaining = this.packetSize;
79
+ while (remaining > 0) {
80
+ if (this.buffers.length === 0) {
81
+ packet.pcm.fill(0, offset);
82
+ remaining = 0;
83
+ break;
84
+ }
85
+ const first = this.buffers[0];
86
+ if (first.length <= remaining) {
87
+ first.copy(packet.pcm, offset);
88
+ offset += first.length;
89
+ remaining -= first.length;
90
+ this.buffers.shift();
91
+ }
92
+ else {
93
+ first.copy(packet.pcm, offset, 0, remaining);
94
+ this.buffers[0] = first.slice(remaining);
95
+ offset += remaining;
96
+ remaining = 0;
97
+ }
98
+ }
99
+ this.currentSize -= this.packetSize;
100
+ if (this.hadUnderrun && this.currentSize >= this.packetSize) {
101
+ this.hadUnderrun = false;
102
+ }
103
+ if (this.status === ENDING && this.currentSize <= 0) {
104
+ this.status = ENDED;
105
+ this.currentSize = 0;
106
+ this.emit('status', 'end');
107
+ }
108
+ if (this.status === DRAINING && this.currentSize < this.maxSize / 2) {
109
+ this.status = NORMAL;
110
+ this.emit('drain');
111
+ }
112
+ }
113
+ if (this.muted) {
114
+ packet.pcm.fill(0);
115
+ }
116
+ return packet;
117
+ }
118
+ /** Mark the buffer as ending; drains then emits `end`. */
119
+ end() {
120
+ if (this.status === FILLING) {
121
+ this.emit('status', 'playing');
122
+ }
123
+ this.status = ENDING;
124
+ }
125
+ /** Clear internal buffers and state. */
126
+ reset() {
127
+ this.buffers = [];
128
+ this.currentSize = 0;
129
+ this.status = WAITING;
130
+ }
131
+ }
132
+ exports.default = CircularBuffer;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.applyConfig = applyConfig;
5
+ const numUtil_1 = require("./numUtil");
6
+ exports.config = {
7
+ user_agent: 'iTunes/11.3.1 (Windows; Microsoft Windows 10 x64 (Build 19044); x64) (dt:2)',
8
+ udp_default_port: 54621, // preferred starting port in AirTunes v2
9
+ frames_per_packet: 352, // samples per frames in ALAC packets
10
+ channels_per_frame: 2, // always stereo in AirTunes v2
11
+ bits_per_channel: 16, // -> 2 bytes per channel
12
+ pcm_packet_size: 352 * 2 * 2, // frames*channels*bytes
13
+ alac_packet_size: 352 * 2 * 2 + 8, // pcm payload + alac header/footer
14
+ packet_size: 352 * 2 * 2, // active packet size (depends on input codec)
15
+ packets_in_buffer: 260, // ~2.1s of audio (matches MA's ~2000ms buffer)
16
+ coreaudio_min_level: 5, // if CoreAudio's internal buffer drops too much, inject some silence to raise it
17
+ coreaudio_check_period: 2000, // CoreAudio buffer level check period
18
+ coreaudio_preload: 352 * 2 * 2 * 50, // ~0.5s of silence pushed to CoreAudio to avoid draining AudioQueue
19
+ sampling_rate: 44100, // fixed by AirTunes v2
20
+ sync_period: 0, // UDP sync packets are sent to all AirTunes devices regularly (0 = derive from sample rate)
21
+ stream_latency: 200, // audio UDP packets are flushed in bursts periodically
22
+ rtsp_timeout: 2147483647, // RTSP servers are considered gone if no reply is received before the timeout (legacy default)
23
+ rtsp_heartbeat: 15000, // some RTSP (like HomePod) servers requires heartbeat.
24
+ rtsp_retry_attempts: 3,
25
+ rtsp_retry_base_ms: 300,
26
+ rtsp_retry_max_ms: 4000,
27
+ rtsp_retry_jitter_ms: 150,
28
+ control_sync_base_delay_ms: 2,
29
+ control_sync_jitter_ms: 3,
30
+ use_monotonic_clock: true,
31
+ jump_forward_enabled: false,
32
+ jump_forward_threshold_ms: 180,
33
+ jump_forward_lead_ms: 220,
34
+ device_magic: (0, numUtil_1.randomInt)(9),
35
+ resend_buffer_size: 1000,
36
+ send_rtcp_sr: true,
37
+ send_rtcp_rr: true,
38
+ send_rtcp_xr: false,
39
+ underrun_mute_ms: 50,
40
+ debug_dump: false,
41
+ ntp_epoch: 0x83aa7e80,
42
+ iv_base64: 'ePRBLI0XN5ArFaaz7ncNZw',
43
+ rsa_aeskey_base64: 'VjVbxWcmYgbBbhwBNlCh3K0CMNtWoB844BuiHGUJT51zQS7SDpMnlbBIobsKbfEJ3SCgWHRXjYWf7VQWRYtEcfx7ejA8xDIk5PSBYTvXP5dU2QoGrSBv0leDS6uxlEWuxBq3lIxCxpWO2YswHYKJBt06Uz9P2Fq2hDUwl3qOQ8oXb0OateTKtfXEwHJMprkhsJsGDrIc5W5NJFMAo6zCiM9bGSDeH2nvTlyW6bfI/Q0v0cDGUNeY3ut6fsoafRkfpCwYId+bg3diJh+uzw5htHDyZ2sN+BFYHzEfo8iv4KDxzeya9llqg6fRNQ8d5YjpvTnoeEQ9ye9ivjkBjcAfVw',
44
+ };
45
+ function applyConfig(overrides) {
46
+ Object.assign(exports.config, overrides);
47
+ return exports.config;
48
+ }
49
+ exports.default = exports.config;