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,27 @@
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 config_1 = __importDefault(require("./config"));
7
+ class NTP {
8
+ timeRef = Date.now() - config_1.default.ntp_epoch * 1000;
9
+ timestamp() {
10
+ const time = Date.now() - this.timeRef;
11
+ const sec = Math.floor(time / 1000);
12
+ const msec = time - sec * 1000;
13
+ const ntp_msec = Math.floor(msec * 4294967.296);
14
+ const ts = Buffer.alloc(8);
15
+ ts.writeUInt32BE(sec, 0);
16
+ ts.writeUInt32BE(ntp_msec, 4);
17
+ return ts;
18
+ }
19
+ getTime() {
20
+ const time = Date.now() - this.timeRef;
21
+ const sec = Math.floor(time / 1000);
22
+ const msec = time - sec * 1000;
23
+ const ntp_msec = Math.floor(msec * 4294967.296);
24
+ return ntp_msec;
25
+ }
26
+ }
27
+ exports.default = new NTP();
@@ -0,0 +1,17 @@
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.low32 = exports.low16 = exports.randomInt = exports.randomBase64 = exports.randomHex = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const randomHex = (n) => node_crypto_1.default.randomBytes(n).toString('hex');
9
+ exports.randomHex = randomHex;
10
+ const randomBase64 = (n) => node_crypto_1.default.randomBytes(n).toString('base64').replace('=', '');
11
+ exports.randomBase64 = randomBase64;
12
+ const randomInt = (n) => Math.floor(Math.random() * Math.pow(10, n));
13
+ exports.randomInt = randomInt;
14
+ const low16 = (i) => Math.abs(i) % 65536;
15
+ exports.low16 = low16;
16
+ const low32 = (i) => Math.abs(i) % 4294967296;
17
+ exports.low32 = low32;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Packet = void 0;
4
+ /**
5
+ * Reusable packet structure holding PCM/ALAC data plus sequence.
6
+ * Reference-counted to reduce allocations in the streaming path.
7
+ */
8
+ class Packet {
9
+ pool;
10
+ ref = 1;
11
+ seq = null;
12
+ pcm;
13
+ constructor(pool, packetSize) {
14
+ this.pool = pool;
15
+ this.pcm = Buffer.alloc(packetSize);
16
+ }
17
+ /** Increment ref count when sharing the packet. */
18
+ retain() {
19
+ this.ref += 1;
20
+ }
21
+ /** Decrement ref count and return to pool when free. */
22
+ release() {
23
+ this.ref -= 1;
24
+ if (this.ref === 0) {
25
+ this.seq = null;
26
+ this.pool.release(this);
27
+ }
28
+ }
29
+ }
30
+ exports.Packet = Packet;
31
+ /** Simple pool of Packet instances to avoid GC pressure during streaming. */
32
+ class PacketPool {
33
+ packetSize;
34
+ pool = [];
35
+ constructor(packetSize) {
36
+ this.packetSize = packetSize;
37
+ }
38
+ /** Borrow a packet from the pool or allocate a new one. */
39
+ getPacket() {
40
+ const packet = this.pool.shift();
41
+ if (!packet) {
42
+ return new Packet(this, this.packetSize);
43
+ }
44
+ packet.retain();
45
+ return packet;
46
+ }
47
+ /** Return a packet to the pool. */
48
+ release(packet) {
49
+ this.pool.push(packet);
50
+ }
51
+ }
52
+ exports.default = PacketPool;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buf2hex = exports.hexString2ArrayBuffer = void 0;
4
+ const hexString2ArrayBuffer = (hexString) => new Uint8Array(hexString.match(/[\da-f]{2}/gi)?.map((h) => parseInt(h, 16)) ?? []);
5
+ exports.hexString2ArrayBuffer = hexString2ArrayBuffer;
6
+ const buf2hex = (buffer) => Array.prototype.map
7
+ .call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2))
8
+ .join('');
9
+ exports.buf2hex = buf2hex;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Holds and serializes HomeKit credential blobs used during AirPlay 2 auth.
3
+ */
4
+ declare class Credentials {
5
+ uniqueIdentifier: string;
6
+ identifier: Buffer;
7
+ pairingId: string;
8
+ publicKey: Buffer;
9
+ encryptionKey: Buffer;
10
+ encryptCount: number;
11
+ decryptCount: number;
12
+ writeKey: Buffer;
13
+ readKey: Buffer;
14
+ constructor(uniqueIdentifier: string, identifier: Buffer, pairingId: string, publicKey: Buffer, encryptionKey: Buffer);
15
+ /**
16
+ * Parse a credentials string into a Credentials object.
17
+ * @param text The credentials string.
18
+ * @returns A credentials object.
19
+ */
20
+ static parse(text: string): Credentials;
21
+ /**
22
+ * Returns a string representation of a Credentials object.
23
+ * @returns A string representation of a Credentials object.
24
+ */
25
+ toString(): string;
26
+ encrypt(message: Buffer): Buffer;
27
+ decrypt(message: Buffer): Buffer;
28
+ encryptAudio(message: Buffer, aad: Buffer | null, nonce: number): Buffer;
29
+ }
30
+ export { Credentials };
@@ -0,0 +1,100 @@
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.Credentials = void 0;
7
+ const encryption_1 = __importDefault(require("./encryption"));
8
+ const struct = require('python-struct');
9
+ /**
10
+ * Holds and serializes HomeKit credential blobs used during AirPlay 2 auth.
11
+ */
12
+ class Credentials {
13
+ uniqueIdentifier;
14
+ identifier;
15
+ pairingId;
16
+ publicKey;
17
+ encryptionKey;
18
+ encryptCount;
19
+ decryptCount;
20
+ writeKey;
21
+ readKey;
22
+ constructor(uniqueIdentifier, identifier, pairingId, publicKey, encryptionKey) {
23
+ this.uniqueIdentifier = uniqueIdentifier;
24
+ this.identifier = identifier;
25
+ this.pairingId = pairingId;
26
+ this.publicKey = publicKey;
27
+ this.encryptionKey = encryptionKey;
28
+ this.encryptCount = 0;
29
+ this.decryptCount = 0;
30
+ this.writeKey = encryptionKey;
31
+ this.readKey = encryptionKey;
32
+ }
33
+ /**
34
+ * Parse a credentials string into a Credentials object.
35
+ * @param text The credentials string.
36
+ * @returns A credentials object.
37
+ */
38
+ static parse(text) {
39
+ const parts = text.split(':');
40
+ return new Credentials(parts[0], Buffer.from(parts[1], 'hex'), Buffer.from(parts[2], 'hex').toString(), Buffer.from(parts[3], 'hex'), Buffer.from(parts[4], 'hex'));
41
+ }
42
+ /**
43
+ * Returns a string representation of a Credentials object.
44
+ * @returns A string representation of a Credentials object.
45
+ */
46
+ toString() {
47
+ return this.uniqueIdentifier
48
+ + ":"
49
+ + this.identifier.toString('hex')
50
+ + ":"
51
+ + Buffer.from(this.pairingId).toString('hex')
52
+ + ":"
53
+ + this.publicKey.toString('hex')
54
+ + ":"
55
+ + this.encryptionKey.toString('hex');
56
+ }
57
+ encrypt(message) {
58
+ let offset = 0;
59
+ const total = message.byteLength;
60
+ let result = Buffer.concat([]);
61
+ while (offset < total) {
62
+ const length = Math.min(total - offset, 1024);
63
+ const s1lengthBytes = struct.pack("H", length);
64
+ // let cipher = crypto.createCipheriv('chacha20-poly1305', this.writeKey, Buffer.concat([Buffer.from([0x00,0x00,0x00,0x00]),struct.pack("Q", this.decryptCount)]), { authTagLength: 16 });
65
+ // cipher.setAAD(s1length_bytes);
66
+ // let s1ct = cipher.update(message);
67
+ // cipher.final();
68
+ // let s1tag = encryption_1.default.computePoly1305(s1ct,s1length_bytes,Buffer.concat([Buffer.from([0x00,0x00,0x00,0x00]),struct.pack("Q", this.decryptCount)]),this.writeKey)
69
+ const [s1ct, s1tag] = encryption_1.default.encryptAndSeal(message.slice(offset, offset + length), s1lengthBytes, Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), struct.pack("Q", this.encryptCount)]), this.writeKey);
70
+ const ciphertext = Buffer.concat([s1lengthBytes, s1ct, s1tag]);
71
+ offset += length;
72
+ this.encryptCount += 1;
73
+ result = Buffer.concat([result, ciphertext]);
74
+ }
75
+ return result;
76
+ }
77
+ decrypt(message) {
78
+ let offset = 0;
79
+ let result = Buffer.concat([]);
80
+ while (offset < message.byteLength) {
81
+ const lengthBytes = message.slice(offset, offset + 2);
82
+ const length = struct.unpack("H", lengthBytes);
83
+ const messagea = message.slice(offset + 2, offset + 2 + length[0] + 16);
84
+ const cipherText = messagea.slice(0, -16);
85
+ const hmac = messagea.slice(-16);
86
+ const decrypted = encryption_1.default.verifyAndDecrypt(cipherText, hmac, lengthBytes, Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), struct.pack("Q", this.decryptCount)]), this.readKey);
87
+ this.decryptCount += 1;
88
+ offset = offset + length[0] + 16 + 2;
89
+ result = Buffer.concat([result, decrypted ?? Buffer.alloc(0)]);
90
+ }
91
+ return result;
92
+ }
93
+ encryptAudio(message, aad, nonce) {
94
+ return Buffer.concat([
95
+ Buffer.concat(encryption_1.default.encryptAndSeal(message, aad, struct.pack("Q", nonce), this.writeKey)),
96
+ Buffer.from(struct.pack("Q", nonce)),
97
+ ]);
98
+ }
99
+ }
100
+ exports.Credentials = Credentials;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * AirPlay 2/HomeKit encryption helpers (ChaCha20-Poly1305 + HKDF) ported from node_airtunes2.
3
+ */
4
+ declare function verifyAndDecrypt(cipherText: Buffer, mac: Buffer, AAD: Buffer | null, nonce: Buffer, key: Buffer): Buffer | null;
5
+ declare function encryptAndSeal(plainText: Buffer, AAD: Buffer | null, nonce: Buffer, key: Buffer): [Buffer, Buffer];
6
+ declare function HKDF(hashAlg: string, salt: Buffer, ikm: Buffer, info: Buffer | string, size: number): Buffer;
7
+ declare const _default: {
8
+ encryptAndSeal: typeof encryptAndSeal;
9
+ verifyAndDecrypt: typeof verifyAndDecrypt;
10
+ HKDF: typeof HKDF;
11
+ };
12
+ export default _default;
@@ -0,0 +1,82 @@
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 crypto_1 = __importDefault(require("crypto"));
7
+ /**
8
+ * AirPlay 2/HomeKit encryption helpers (ChaCha20-Poly1305 + HKDF) ported from node_airtunes2.
9
+ */
10
+ // i'd really prefer for this to be a direct call to
11
+ // Sodium.crypto_aead_chacha20poly1305_decrypt()
12
+ // but unfortunately the way it constructs the message to
13
+ // calculate the HMAC is not compatible with homekit
14
+ // (long story short, it uses [ AAD, AAD.length, CipherText, CipherText.length ]
15
+ // whereas homekit expects [ AAD, CipherText, AAD.length, CipherText.length ]
16
+ function verifyAndDecrypt(cipherText, mac, AAD, nonce, key) {
17
+ try {
18
+ let nonceBuf = nonce;
19
+ if (nonceBuf.byteLength === 8) {
20
+ nonceBuf = Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), nonceBuf]);
21
+ }
22
+ const decipher = crypto_1.default.createDecipheriv('chacha20-poly1305', key, nonceBuf, { authTagLength: 16 });
23
+ if (AAD != null) {
24
+ decipher.setAAD(AAD); // must be called before data
25
+ }
26
+ decipher.setAuthTag(mac);
27
+ const decrypted = Buffer.concat([decipher.update(cipherText), decipher.final()]);
28
+ return decrypted;
29
+ }
30
+ catch (error) {
31
+ return null;
32
+ }
33
+ }
34
+ function encryptAndSeal(plainText, AAD, nonce, key) {
35
+ let nonceBuf = nonce;
36
+ if (nonceBuf.byteLength === 8) {
37
+ nonceBuf = Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), nonceBuf]);
38
+ }
39
+ const cipher = crypto_1.default.createCipheriv('chacha20-poly1305', key, nonceBuf, { authTagLength: 16 });
40
+ if (AAD != null) {
41
+ cipher.setAAD(AAD); // must be called before data
42
+ }
43
+ const cipherText = Buffer.concat([cipher.update(plainText), cipher.final()]);
44
+ const hmac = cipher.getAuthTag();
45
+ return [cipherText, hmac];
46
+ }
47
+ // function getPadding(buffer, blockSize) {
48
+ // return buffer.length % blockSize === 0
49
+ // ? Buffer.alloc(0)
50
+ // : Buffer.alloc(blockSize - (buffer.length % blockSize));
51
+ // }
52
+ function HKDF(hashAlg, salt, ikm, info, size) {
53
+ // create the hash alg to see if it exists and get its length
54
+ const hash = crypto_1.default.createHash(hashAlg);
55
+ const hashLength = hash.digest().length;
56
+ // now we compute the PRK
57
+ const hmac = crypto_1.default.createHmac(hashAlg, salt);
58
+ hmac.update(ikm);
59
+ const prk = hmac.digest();
60
+ let prev = Buffer.alloc(0);
61
+ const buffers = [];
62
+ const numBlocks = Math.ceil(size / hashLength);
63
+ const infoBuf = Buffer.from(info);
64
+ for (let i = 0; i < numBlocks; i++) {
65
+ const roundHmac = crypto_1.default.createHmac(hashAlg, prk);
66
+ const input = Buffer.concat([
67
+ prev,
68
+ infoBuf,
69
+ Buffer.from(String.fromCharCode(i + 1)),
70
+ ]);
71
+ roundHmac.update(input);
72
+ prev = roundHmac.digest();
73
+ buffers.push(prev);
74
+ }
75
+ const output = Buffer.concat(buffers, size);
76
+ return output.slice(0, size);
77
+ }
78
+ exports.default = {
79
+ encryptAndSeal,
80
+ verifyAndDecrypt,
81
+ HKDF,
82
+ };
@@ -0,0 +1,7 @@
1
+ declare function UInt53toBufferLE(value: number): Buffer;
2
+ declare function UInt16toBufferBE(value: number): Buffer;
3
+ declare const _default: {
4
+ UInt53toBufferLE: typeof UInt53toBufferLE;
5
+ UInt16toBufferBE: typeof UInt16toBufferBE;
6
+ };
7
+ export default _default;
@@ -0,0 +1,47 @@
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 assert_1 = __importDefault(require("assert"));
7
+ /*
8
+ * Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used
9
+ * used per the terms of the Apache Software License v2.
10
+ *
11
+ * Original code copyright Khaos Tian <khaos.tian@gmail.com>
12
+ *
13
+ * Modifications copyright Zach Bean <zb@forty2.com>
14
+ * * Reformatted for ES6-style module
15
+ * * renamed *UInt64* to *UInt53* to be more clear about range
16
+ * * renamed uintHighLow to be more clear about what it does
17
+ * * Refactored to return a buffer rather write into a passed-in buffer
18
+ */
19
+ function splitUInt53(value) {
20
+ const MAX_UINT32 = 0x00000000ffffffff;
21
+ const MAX_INT53 = 0x001fffffffffffff;
22
+ (0, assert_1.default)(value > -1 && value <= MAX_INT53, 'number out of range');
23
+ (0, assert_1.default)(Math.floor(value) === value, 'number must be an integer');
24
+ let high = 0;
25
+ const signbit = value & 0xffffffff;
26
+ const low = signbit < 0 ? (value & 0x7fffffff) + 0x80000000 : signbit;
27
+ if (value > MAX_UINT32) {
28
+ high = (value - low) / (MAX_UINT32 + 1);
29
+ }
30
+ return [high, low];
31
+ }
32
+ function UInt53toBufferLE(value) {
33
+ const [high, low] = splitUInt53(value);
34
+ const buf = Buffer.alloc(8);
35
+ buf.writeUInt32LE(low, 0);
36
+ buf.writeUInt32LE(high, 4);
37
+ return buf;
38
+ }
39
+ function UInt16toBufferBE(value) {
40
+ const buf = Buffer.alloc(2);
41
+ buf.writeUInt16BE(value, 0);
42
+ return buf;
43
+ }
44
+ exports.default = {
45
+ UInt53toBufferLE,
46
+ UInt16toBufferBE,
47
+ };
@@ -0,0 +1,25 @@
1
+ type TLVValue = Buffer | string | number;
2
+ type TLVMap = Record<number, Buffer>;
3
+ type TLVArgs = Array<number | TLVValue>;
4
+ declare function encode(type: number, data: TLVValue, ...args: TLVArgs): Buffer;
5
+ declare function decode(data: Buffer): TLVMap;
6
+ declare const _default: {
7
+ Tag: {
8
+ readonly PairingMethod: 0;
9
+ readonly Username: 1;
10
+ readonly Salt: 2;
11
+ readonly PublicKey: 3;
12
+ readonly Proof: 4;
13
+ readonly EncryptedData: 5;
14
+ readonly Sequence: 6;
15
+ readonly ErrorCode: 7;
16
+ readonly BackOff: 8;
17
+ readonly Signature: 10;
18
+ readonly MFiCertificate: 9;
19
+ readonly MFiSignature: 10;
20
+ readonly Flags: 19;
21
+ };
22
+ encode: typeof encode;
23
+ decode: typeof decode;
24
+ };
25
+ export default _default;
@@ -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,109 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Readable } from 'node:stream';
3
+ /**
4
+ * Configuration for a single AirPlay sender.
5
+ */
6
+ export interface LoxAirplaySenderOptions {
7
+ host: string;
8
+ /** RAOP port (defaults to 5000). */
9
+ port?: number;
10
+ /** Display name shown on the receiver. */
11
+ name?: string;
12
+ /** AirPlay 1 password; null disables auth. */
13
+ password?: string | null;
14
+ /** Initial volume 0–100 (default 50). */
15
+ volume?: number;
16
+ /** Explicit RAOP mode; defaults based on airplay2 flag. */
17
+ mode?: number;
18
+ /** Additional TXT records to advertise. */
19
+ txt?: string[];
20
+ /** Force ALAC encoding even when input is ALAC. */
21
+ forceAlac?: boolean;
22
+ /** Enable ALAC encoding pipeline. */
23
+ alacEncoding?: boolean;
24
+ /** Input format; pcm triggers encoding, alac passes through. */
25
+ inputCodec?: 'pcm' | 'alac';
26
+ /** Enable AirPlay 2 authentication + flags. */
27
+ airplay2?: boolean;
28
+ /** Emit verbose transport logs. */
29
+ debug?: boolean;
30
+ /** Optional unix ms start time for synced playback. */
31
+ startTimeMs?: number;
32
+ /** Logger hook for internal messages. */
33
+ log?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: unknown) => void;
34
+ }
35
+ /**
36
+ * Metadata sent to receivers for UI display.
37
+ */
38
+ export interface AirplayMetadata {
39
+ title?: string;
40
+ artist?: string;
41
+ album?: string;
42
+ cover?: {
43
+ data: Buffer;
44
+ mime?: string;
45
+ };
46
+ coverUrl?: string;
47
+ elapsedMs?: number;
48
+ durationMs?: number;
49
+ }
50
+ export interface LoxAirplayEvent {
51
+ event: string;
52
+ message?: string;
53
+ detail?: any;
54
+ }
55
+ export declare class LoxAirplaySender extends EventEmitter {
56
+ private airtunes;
57
+ private deviceKey;
58
+ private started;
59
+ private source;
60
+ private log?;
61
+ private lastTrackKey;
62
+ private lastCoverKey;
63
+ private lastProgressKey;
64
+ private lastCoverUrl;
65
+ private coverFetch?;
66
+ private artworkTimer?;
67
+ private pendingArtwork?;
68
+ private lastTrackChangeAt;
69
+ /**
70
+ * Create + start a sender for a single AirPlay device.
71
+ * Returns true when the pipeline initializes; safe to call multiple times to restart.
72
+ */
73
+ start(options: LoxAirplaySenderOptions, onEvent?: (event: LoxAirplayEvent) => void): boolean;
74
+ /**
75
+ * Push raw PCM or ALAC frames into the stream.
76
+ */
77
+ sendPcm(chunk: Buffer): void;
78
+ /**
79
+ * Pipe a readable stream into the sender; auto-stops on end/error.
80
+ */
81
+ pipeStream(stream: Readable): void;
82
+ /** Adjust receiver volume (0–100). */
83
+ setVolume(volume: number): void;
84
+ /** Update track metadata immediately without artwork/progress. */
85
+ setTrackInfo(title: string, artist?: string, album?: string): void;
86
+ /** Send cover art immediately. */
87
+ setArtwork(art: Buffer, contentType?: string): void;
88
+ /** Send playback progress in seconds (elapsed, duration). */
89
+ setProgress(progress: number, duration: number): void;
90
+ /**
91
+ * Convenience to send track info, cover (buffer or URL), and progress.
92
+ * Deduplicates payloads and staggers artwork on track changes.
93
+ */
94
+ setMetadata(payload: AirplayMetadata): Promise<void>;
95
+ /** Provide a passcode when a receiver requests it. */
96
+ setPasscode(passcode: string): void;
97
+ /**
98
+ * Stop streaming and tear down state/sockets. Safe to call multiple times.
99
+ */
100
+ stop(): void;
101
+ private fetchCover;
102
+ private sendArtworkNow;
103
+ private queueArtwork;
104
+ }
105
+ /**
106
+ * Convenience helper to construct + start a sender in one call.
107
+ */
108
+ export declare function start(options: LoxAirplaySenderOptions, onEvent?: (event: LoxAirplayEvent) => void): LoxAirplaySender;
109
+ export default LoxAirplaySender;