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,148 @@
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_net_1 = __importDefault(require("node:net"));
7
+ const HttpMessage = (parseStartLine, writeStartLine) => {
8
+ const instance = {
9
+ parse: () => ({ headers: {} }),
10
+ write: () => Buffer.alloc(0),
11
+ };
12
+ instance.parse = (buffer) => {
13
+ const messageObject = { headers: {} };
14
+ // ...
15
+ let bodyIndex = buffer.indexOf('\r\n\r\n');
16
+ let headerString = buffer.slice(0, bodyIndex).toString();
17
+ let body = buffer.slice(bodyIndex + 4);
18
+ headerString = headerString.replace(/\r\n/g, '\n');
19
+ const lines = headerString.split('\n');
20
+ bodyIndex += 2;
21
+ // ...
22
+ let line = lines.shift();
23
+ if (line) {
24
+ parseStartLine(line, messageObject);
25
+ }
26
+ // ...
27
+ line = lines.shift();
28
+ while (line) {
29
+ const headerName = line.substr(0, line.indexOf(':'));
30
+ const headerValue = line.substr(line.indexOf(':') + 1);
31
+ messageObject.headers[headerName] = headerValue.trim();
32
+ line = lines.shift();
33
+ }
34
+ // ...
35
+ if (messageObject.headers['Content-Length'] && messageObject.headers['Content-Length'] !== '0') {
36
+ messageObject.body = body;
37
+ }
38
+ return messageObject;
39
+ };
40
+ instance.write = (messageObject) => {
41
+ let messageString = writeStartLine(messageObject);
42
+ messageString += '\r\n';
43
+ if (messageObject.body) {
44
+ messageObject.headers['Content-Length'] = String(Buffer.byteLength(messageObject.body));
45
+ }
46
+ for (const header in messageObject.headers) {
47
+ messageString += `${header}: ${messageObject.headers[header]}\r\n`;
48
+ }
49
+ messageString += '\r\n';
50
+ const buffer = Buffer.from(messageString);
51
+ if (!messageObject.body) {
52
+ return buffer;
53
+ }
54
+ return Buffer.concat([buffer, messageObject.body], buffer.length + messageObject.body.length);
55
+ };
56
+ return instance;
57
+ };
58
+ const HttpRequest = () => HttpMessage(() => { }, // currently not parsing requests.
59
+ (messageObject) => `${messageObject.method} ${messageObject.path} HTTP/1.1`);
60
+ const HttpResponse = () => HttpMessage((line, messageObject) => {
61
+ messageObject.statusCode = parseInt(line.split(' ')[1], 10);
62
+ }, () => '');
63
+ // ...
64
+ class HttpClient {
65
+ resolveQueue = [];
66
+ pendingResponse = null;
67
+ socket;
68
+ host;
69
+ // ....
70
+ parseResponse(data) {
71
+ const res = HttpResponse().parse(data);
72
+ if (res.headers['Content-Length'] && Number(res.headers['Content-Length']) > 0) {
73
+ const remaining = Number(res.headers['Content-Length']) - (res.body?.byteLength ?? 0);
74
+ if (remaining > 0) {
75
+ // not all data for this response's corresponding request was read. Create a pending response object
76
+ // to use for further reads.
77
+ this.pendingResponse = {
78
+ res,
79
+ remaining
80
+ };
81
+ }
82
+ }
83
+ if (!this.pendingResponse) {
84
+ const rr = this.resolveQueue.shift();
85
+ if (!rr)
86
+ return;
87
+ res.statusCode === 200
88
+ ? rr.resolve(res)
89
+ : rr.resolve(null);
90
+ }
91
+ }
92
+ // ...
93
+ connect(host, port = 80) {
94
+ this.host = host;
95
+ return new Promise(resolve => {
96
+ this.socket = node_net_1.default.connect({
97
+ host,
98
+ port
99
+ }, resolve);
100
+ this.socket.on('data', data => {
101
+ if (!this.pendingResponse) {
102
+ // there is no response pending, parse the data.
103
+ this.parseResponse(data);
104
+ }
105
+ else {
106
+ // incoming data for the pending response.
107
+ const existing = this.pendingResponse.res.body ?? Buffer.alloc(0);
108
+ this.pendingResponse.res.body = Buffer.concat([existing, data], data.byteLength + existing.byteLength);
109
+ this.pendingResponse.remaining -= data.byteLength;
110
+ if (this.pendingResponse.remaining === 0) {
111
+ // all remaining data for the pending response has been read; resolve the promise for the
112
+ // corresponding request.
113
+ const rr = this.resolveQueue.shift();
114
+ if (!rr) {
115
+ this.pendingResponse = null;
116
+ return;
117
+ }
118
+ this.pendingResponse.res.statusCode === 200
119
+ ? rr.resolve(this.pendingResponse.res)
120
+ : rr.reject(new Error(`HTTP status: ${this.pendingResponse.res.statusCode}`));
121
+ this.pendingResponse = null;
122
+ }
123
+ }
124
+ });
125
+ });
126
+ }
127
+ request(method, path, headers, body) {
128
+ headers = headers || {};
129
+ // headers['Host'] = `${this.host}:${this.socket.remotePort}`;
130
+ const data = HttpRequest().write({
131
+ method,
132
+ path,
133
+ headers,
134
+ body
135
+ });
136
+ // ...
137
+ return new Promise((resolve, reject) => {
138
+ this.resolveQueue.push({ resolve, reject });
139
+ this.socket?.write(data);
140
+ });
141
+ }
142
+ close() {
143
+ this.socket?.end();
144
+ }
145
+ }
146
+ // ...
147
+ const createHttpClient = () => new HttpClient();
148
+ exports.default = createHttpClient;
@@ -0,0 +1,7 @@
1
+ declare class NTP {
2
+ private readonly timeRef;
3
+ timestamp(): Buffer;
4
+ getTime(): number;
5
+ }
6
+ declare const _default: NTP;
7
+ export default _default;
@@ -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,5 @@
1
+ export declare const randomHex: (n: number) => string;
2
+ export declare const randomBase64: (n: number) => string;
3
+ export declare const randomInt: (n: number) => number;
4
+ export declare const low16: (i: number) => number;
5
+ export declare const low32: (i: number) => number;
@@ -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,25 @@
1
+ /**
2
+ * Reusable packet structure holding PCM/ALAC data plus sequence.
3
+ * Reference-counted to reduce allocations in the streaming path.
4
+ */
5
+ export declare class Packet {
6
+ private readonly pool;
7
+ private ref;
8
+ seq: number | null;
9
+ readonly pcm: Buffer;
10
+ constructor(pool: PacketPool, packetSize: number);
11
+ /** Increment ref count when sharing the packet. */
12
+ retain(): void;
13
+ /** Decrement ref count and return to pool when free. */
14
+ release(): void;
15
+ }
16
+ /** Simple pool of Packet instances to avoid GC pressure during streaming. */
17
+ export default class PacketPool {
18
+ private readonly packetSize;
19
+ private readonly pool;
20
+ constructor(packetSize: number);
21
+ /** Borrow a packet from the pool or allocate a new one. */
22
+ getPacket(): Packet;
23
+ /** Return a packet to the pool. */
24
+ release(packet: Packet): void;
25
+ }
@@ -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,2 @@
1
+ export declare const hexString2ArrayBuffer: (hexString: string) => Uint8Array;
2
+ export declare const buf2hex: (buffer: ArrayBuffer | Uint8Array) => string;
@@ -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;
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "lox-airplay-sender",
3
+ "version": "0.1.0",
4
+ "description": "AirPlay sender (RAOP/AirPlay 1 + AirPlay 2 auth flows) ",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/esm/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist/**/*", "README.md", "LICENSE"],
16
+ "scripts": {
17
+ "build": "npm run build:cjs && npm run build:esm",
18
+ "build:cjs": "tsc -p tsconfig.json",
19
+ "build:esm": "tsc -p tsconfig.esm.json && node -e \"const fs=require('fs');fs.mkdirSync('dist/esm',{recursive:true});fs.writeFileSync('dist/esm/package.json','{\\\"type\\\":\\\"module\\\"}\\n');\"",
20
+ "clean": "rimraf dist",
21
+ "prepare": "npm run build"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/rudyberends/lox-airplay-sender.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/rudyberends/lox-airplay-sender/issues"
29
+ },
30
+ "homepage": "https://github.com/rudyberends/lox-airplay-sender#readme",
31
+ "keywords": ["airplay", "raop", "audio", "streaming"],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@noble/ed25519": "^1.7.3",
40
+ "async": "^3.2.5",
41
+ "big-integer": "^1.6.52",
42
+ "bplist-creator": "^0.1.1",
43
+ "bplist-parser": "^0.3.2",
44
+ "crypto-js": "^4.2.0",
45
+ "curve25519-js": "^0.0.4",
46
+ "elliptic": "^6.5.5",
47
+ "fast-srp-hap": "^2.0.4",
48
+ "js-crypto-aes": "^1.0.6",
49
+ "js-sha1": "^0.7.0",
50
+ "lodash": "^4.17.21",
51
+ "parse-raw-http": "0.0.1",
52
+ "python-struct": "^1.1.3",
53
+ "simple-plist": "^1.4.0",
54
+ "varint": "^6.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.19.3",
58
+ "rimraf": "^6.0.1",
59
+ "typescript": "^5.9.3"
60
+ },
61
+ "license": "AGPL-3.0"
62
+ }