@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
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @lox-audioserver/node-airplay-sender
2
+
3
+ AirPlay sender for RAOP/AirPlay 1 with AirPlay 2 control/auth support. Best-effort AP2 audio (encryption path not validated against native code). Implements RTSP/UDP pipeline, ALAC encoding, and metadata handling with no native dependencies.
4
+
5
+ ## Requirements
6
+ - Node.js 18+
7
+ - PCM input: 16-bit little-endian, stereo, 44.1kHz (ALAC encoding is handled internally)
8
+
9
+ ## Installation
10
+ ```bash
11
+ npm install lox-airplay-sender
12
+ ```
13
+
14
+ ## Quick start
15
+ ```ts
16
+ import { start } from "lox-airplay-sender";
17
+
18
+ const sender = start(
19
+ {
20
+ host: "192.168.1.162",
21
+ port: 7000, // defaults to 5000
22
+ airplay2: true, // set to true for AirPlay 2 devices
23
+ log: (level, msg, data) => console.log(`[${level}]`, msg, data),
24
+ },
25
+ (event) => console.log("event", event)
26
+ );
27
+
28
+ sender.setMetadata({
29
+ title: "Track",
30
+ artist: "Artist",
31
+ album: "Album",
32
+ coverUrl: "https://example.com/cover.jpg",
33
+ durationMs: 180_000,
34
+ elapsedMs: 0,
35
+ });
36
+
37
+ // Write raw PCM chunks as they arrive
38
+ sender.sendPcm(Buffer.from(/* pcm data */));
39
+ ```
40
+
41
+ ## API
42
+ ### `start(options, onEvent?) => LoxAirplaySender`
43
+ Creates and starts a sender for one AirPlay device. Returns the instance so you can call methods directly.
44
+
45
+ **Options**
46
+ - `host` (string, required) AirPlay device hostname/IP.
47
+ - `port` (number) RAOP port, default 5000.
48
+ - `name` (string) Sender name shown on receiver.
49
+ - `password` (string | null) AirPlay 1 password.
50
+ - `volume` (number) Initial volume (0–100), default 50.
51
+ - `mode` (number) RAOP mode; defaults to 2 when `airplay2` is true, else 0.
52
+ - `txt` (string[]) Custom TXT records.
53
+ - `forceAlac` (boolean) Encode ALAC even when input is ALAC; default true.
54
+ - `alacEncoding` (boolean) Enable ALAC encoding; default true.
55
+ - `inputCodec` (`"pcm"` | `"alac"`) Defaults to `"pcm"`.
56
+ - `airplay2` (boolean) Enable AirPlay 2 auth/flags; default false.
57
+ - `startTimeMs` (number) Unix ms to align playback across devices.
58
+ - `startTimeNtp` (bigint | number | `{ sec, frac }`) Absolute NTP start time (sec<<32|frac) for tighter sync.
59
+ - `deviceMagic` (number) Override per-session SSRC/device ID used in RTP/RTCP.
60
+ - `resendBufferSize` (number) Override resend cache size (packets); defaults to 1000.
61
+ - `underrunMuteMs` (number) Mute window in ms after an underrun to smooth recovery; defaults to 50.
62
+ - `sendRtcpXr` (boolean) Emit RTCP Extended Reports (RRT) alongside sync/SSR; default false.
63
+ - `debugDump` (boolean) Log RTSP/RTCP traffic for debugging; default false.
64
+ - `debug` (boolean) Verbose logging from the transport stack.
65
+ - `log` `(level, message, data?) => void` Hook for library logs.
66
+ - `config` (partial) Override buffer/sync/RTSP tuning at runtime (see `src/utils/config.ts` for keys like `packets_in_buffer`, `stream_latency`, `sync_period`, retry/backoff, etc.).
67
+
68
+ **Events** (sent to `onEvent` callback)
69
+ - `device`: `{ event: "device", message: status, detail: { key, desc } }`
70
+ - `buffer`: `{ event: "buffer", message: status }` where status is `buffering|playing|drain|end`
71
+ - `error`: `{ event: "error", message }`
72
+ - `metrics`: `{ event: "metrics", detail }` sync drift snapshots emitted on each sync tick when enabled.
73
+
74
+ ### `LoxAirplaySender` methods
75
+ - `sendPcm(chunk: Buffer)`: Push raw PCM audio. If `inputCodec` is `"alac"` you can push ALAC frames.
76
+ - `pipeStream(stream: Readable)`: Convenience to pipe a Node stream into `sendPcm`; auto-stops on `end`/`error`.
77
+ - `setMetadata({ title, artist, album, cover, coverUrl, elapsedMs, durationMs })`: Updates track info, cover art (Buffer or URL), and progress. Cover URLs are fetched with a short timeout and deduplicated.
78
+ - `setTrackInfo(title, artist?, album?)`: Direct track update.
79
+ - `setArtwork(buffer, mime?)`: Send cover art immediately.
80
+ - `setProgress(elapsedSec, durationSec)`: Manual progress update.
81
+ - `setVolume(volume)`: Adjust volume (0–100).
82
+ - `setPasscode(passcode)`: Provide a passcode when the receiver requests it.
83
+ - `stop()`: Stop the sender, close sockets/streams, and clear state.
84
+
85
+ ## Sync playback
86
+ Use `startTimeMs` to align multiple senders to the same start clock (Unix ms). Feed each sender PCM in lockstep; they will start at the scheduled time.
87
+
88
+ ## Development
89
+ ```bash
90
+ npm install
91
+ npm run build
92
+ npm run clean # remove dist
93
+ ```
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
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 number_1 = __importDefault(require("../homekit/number"));
7
+ const u = number_1.default.UInt16toBufferBE(287);
8
+ console.log(Buffer.concat([u.slice(1), u.slice(0, 1)]));
@@ -0,0 +1,16 @@
1
+ import httpClientFactory from '../utils/http';
2
+ declare class ATV {
3
+ addr: string;
4
+ port: number;
5
+ httpClient: ReturnType<typeof httpClientFactory>;
6
+ auth_secret: string | null;
7
+ constructor(addr: string, port?: number);
8
+ auth(configFilePath: string, authenticator: () => Promise<string>): Promise<import("../utils/http").MessageObject | null>;
9
+ authSecret(): string | null;
10
+ authSimple(authenticator: () => Promise<string>): Promise<void>;
11
+ verifySimple(secret: string): Promise<import("../utils/http").MessageObject | null>;
12
+ play(videoUrl: string): Promise<import("../utils/http").MessageObject | null>;
13
+ stop(): Promise<import("../utils/http").MessageObject | null>;
14
+ close(): void;
15
+ }
16
+ export default ATV;
@@ -0,0 +1,215 @@
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
+ const fs_1 = __importDefault(require("fs"));
8
+ const bplist_creator_1 = __importDefault(require("bplist-creator"));
9
+ const bplist_parser_1 = __importDefault(require("bplist-parser"));
10
+ const srp_1 = __importDefault(require("./srp"));
11
+ const atvAuthenticator_1 = __importDefault(require("./atvAuthenticator"));
12
+ const http_1 = __importDefault(require("../utils/http"));
13
+ // ...
14
+ // Configuration.
15
+ const loadConfig = (configFilePath) => !fs_1.default.existsSync(configFilePath) ? null : JSON.parse(fs_1.default.readFileSync(configFilePath, 'utf8'));
16
+ const saveConfig = (configFilePath, config) => fs_1.default.writeFileSync(configFilePath, JSON.stringify(config, null, '\t'));
17
+ // ...
18
+ class ATV {
19
+ addr;
20
+ port;
21
+ httpClient;
22
+ auth_secret;
23
+ constructor(addr, port) {
24
+ this.addr = addr;
25
+ this.port = port || 7000;
26
+ this.httpClient = (0, http_1.default)();
27
+ this.auth_secret = null;
28
+ }
29
+ // ...
30
+ auth(configFilePath, authenticator) {
31
+ async function auth(owner) {
32
+ await owner.httpClient.connect(owner.addr, owner.port);
33
+ let conf = loadConfig(configFilePath);
34
+ const authSecret = conf && typeof conf['auth_secret'] === 'string' ? conf['auth_secret'] : null;
35
+ if (!authSecret) {
36
+ // a pairing does not exist and must be performed.
37
+ // ...
38
+ // SRP parameters.
39
+ const srp = new srp_1.default(2048);
40
+ const I = '366B4165DD64AD3A';
41
+ let P;
42
+ let s;
43
+ let B;
44
+ let a;
45
+ let A;
46
+ let M1;
47
+ await owner.httpClient.request('POST', '/pair-pin-start')
48
+ .then(() => authenticator())
49
+ .then(pin => {
50
+ P = pin;
51
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
52
+ 'Content-Type': 'application/x-apple-binary-plist'
53
+ }, (0, bplist_creator_1.default)({
54
+ user: '366B4165DD64AD3A',
55
+ method: 'pin'
56
+ }));
57
+ })
58
+ .then((res) => {
59
+ const { pk, salt } = bplist_parser_1.default.parseBuffer(res.body)[0];
60
+ s = salt.toString('hex');
61
+ B = pk.toString('hex');
62
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
63
+ // subsequent session authentication(s).
64
+ a = crypto_1.default.randomBytes(32).toString('hex');
65
+ // SRP: Compute A and M1.
66
+ A = srp.A(a);
67
+ M1 = srp.M1(I, P, s, a, B);
68
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
69
+ 'Content-Type': 'application/x-apple-binary-plist'
70
+ }, (0, bplist_creator_1.default)({
71
+ pk: Buffer.from(A, 'hex'),
72
+ proof: Buffer.from(M1, 'hex')
73
+ }));
74
+ })
75
+ .then(() => {
76
+ // confirm the auth secret (a).
77
+ const { epk, authTag } = atvAuthenticator_1.default.confirm(a, srp.K(I, P, s, a, B));
78
+ // complete pair-setup-pin by registering the auth secret with the target device.
79
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
80
+ 'Content-Type': 'application/x-apple-binary-plist'
81
+ }, (0, bplist_creator_1.default)({
82
+ epk: Buffer.from(epk, 'hex'),
83
+ authTag: Buffer.from(authTag, 'hex')
84
+ }));
85
+ })
86
+ .then(() => {
87
+ // save the auth secret for subsequent session authentication(s).
88
+ if (!conf) {
89
+ conf = {};
90
+ }
91
+ conf['auth_secret'] = a;
92
+ saveConfig(configFilePath, conf);
93
+ });
94
+ }
95
+ // ...
96
+ // Authenticate session with the target device using existing pairing information.
97
+ const verifier = atvAuthenticator_1.default.verifier(conf?.['auth_secret'] ?? '');
98
+ return owner.httpClient.request('POST', '/pair-verify', {
99
+ 'Content-Type': 'application/octet-stream'
100
+ }, verifier.verifierBody)
101
+ .then((res) => {
102
+ const atv_pub = res.body.slice(0, 32).toString('hex');
103
+ const atv_data = res.body.slice(32).toString('hex');
104
+ const shared = atvAuthenticator_1.default.shared(verifier.v_pri, atv_pub);
105
+ const signed = atvAuthenticator_1.default.signed(conf?.['auth_secret'] ?? '', verifier.v_pub, atv_pub);
106
+ const signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
107
+ atvAuthenticator_1.default.signature(shared, atv_data, signed), 'hex');
108
+ return owner.httpClient.request('POST', '/pair-verify', {
109
+ 'Content-Type': 'application/octet-stream'
110
+ }, signature);
111
+ });
112
+ }
113
+ return auth(this);
114
+ }
115
+ authSecret() {
116
+ return this.auth_secret;
117
+ }
118
+ authSimple(authenticator) {
119
+ async function auth(owner) {
120
+ await owner.httpClient.connect(owner.addr, owner.port);
121
+ const conf = null;
122
+ if (!conf || !conf['auth_secret']) {
123
+ // a pairing does not exist and must be performed.
124
+ // ...
125
+ // SRP parameters.
126
+ const srp = new srp_1.default(2048);
127
+ const I = '366B4165DD64AD3A';
128
+ let P;
129
+ let s;
130
+ let B;
131
+ let a;
132
+ let A;
133
+ let M1;
134
+ await owner.httpClient.request('POST', '/pair-pin-start')
135
+ .then(() => authenticator())
136
+ .then(pin => {
137
+ P = pin;
138
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
139
+ 'Content-Type': 'application/x-apple-binary-plist'
140
+ }, (0, bplist_creator_1.default)({
141
+ user: '366B4165DD64AD3A',
142
+ method: 'pin'
143
+ }));
144
+ })
145
+ .then((res) => {
146
+ const { pk, salt } = bplist_parser_1.default.parseBuffer(res.body)[0];
147
+ s = salt.toString('hex');
148
+ B = pk.toString('hex');
149
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
150
+ // subsequent session authentication(s).
151
+ a = crypto_1.default.randomBytes(32).toString('hex');
152
+ // SRP: Compute A and M1.
153
+ A = srp.A(a);
154
+ M1 = srp.M1(I, P, s, a, B);
155
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
156
+ 'Content-Type': 'application/x-apple-binary-plist'
157
+ }, (0, bplist_creator_1.default)({
158
+ pk: Buffer.from(A, 'hex'),
159
+ proof: Buffer.from(M1, 'hex')
160
+ }));
161
+ }).then(() => {
162
+ // confirm the auth secret (a).
163
+ const { epk, authTag } = atvAuthenticator_1.default.confirm(a, srp.K(I, P, s, a, B));
164
+ // complete pair-setup-pin by registering the auth secret with the target device.
165
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
166
+ 'Content-Type': 'application/x-apple-binary-plist'
167
+ }, (0, bplist_creator_1.default)({
168
+ epk: Buffer.from(epk, 'hex'),
169
+ authTag: Buffer.from(authTag, 'hex')
170
+ }));
171
+ })
172
+ .then(() => {
173
+ // save the auth secret for subsequent session authentication(s).
174
+ owner.auth_secret = a;
175
+ });
176
+ }
177
+ }
178
+ return auth(this);
179
+ }
180
+ verifySimple(secret) {
181
+ // ...
182
+ // Authenticate session with the target device using existing pairing information.
183
+ const verifier = atvAuthenticator_1.default.verifier(secret);
184
+ return this.httpClient.request('POST', '/pair-verify', {
185
+ 'Content-Type': 'application/octet-stream',
186
+ }, verifier.verifierBody)
187
+ .then((res) => {
188
+ const atv_pub = res.body.slice(0, 32).toString('hex');
189
+ const atv_data = res.body.slice(32).toString('hex');
190
+ const shared = atvAuthenticator_1.default.shared(verifier.v_pri, atv_pub);
191
+ const signed = atvAuthenticator_1.default.signed(secret, verifier.v_pub, atv_pub);
192
+ const signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
193
+ atvAuthenticator_1.default.signature(shared, atv_data, signed), 'hex');
194
+ return this.httpClient.request('POST', '/pair-verify', {
195
+ 'Content-Type': 'application/octet-stream',
196
+ }, signature);
197
+ });
198
+ }
199
+ play(videoUrl) {
200
+ return this.httpClient.request('POST', '/play', {
201
+ 'Content-Type': 'application/x-apple-binary-plist'
202
+ }, (0, bplist_creator_1.default)({
203
+ 'Content-Location': videoUrl,
204
+ 'Start-Location': 0
205
+ }));
206
+ }
207
+ stop() {
208
+ return this.httpClient.request('POST', '/stop');
209
+ }
210
+ close() {
211
+ this.httpClient.close();
212
+ }
213
+ }
214
+ // ...
215
+ exports.default = ATV;
@@ -0,0 +1,30 @@
1
+ declare function pair_setup_aes_key(K: string): string;
2
+ declare function pair_setup_aes_iv(K: string): string;
3
+ declare function pair_verify_aes_key(shared: string): string;
4
+ declare function pair_verify_aes_iv(shared: string): string;
5
+ declare function a_pub(a: string): string;
6
+ declare function confirm(a: string, K: string): {
7
+ epk: string;
8
+ authTag: string;
9
+ };
10
+ declare function verifier(a: string): {
11
+ verifierBody: Buffer;
12
+ v_pri: string;
13
+ v_pub: string;
14
+ };
15
+ declare function shared(v_pri: string, atv_pub: string): string;
16
+ declare function signed(a: string, v_pub: string, atv_pub: string): string;
17
+ declare function signature(shared: string, atv_data: string, signed: string): string;
18
+ declare const _default: {
19
+ pair_setup_aes_key: typeof pair_setup_aes_key;
20
+ pair_setup_aes_iv: typeof pair_setup_aes_iv;
21
+ pair_verify_aes_key: typeof pair_verify_aes_key;
22
+ pair_verify_aes_iv: typeof pair_verify_aes_iv;
23
+ a_pub: typeof a_pub;
24
+ confirm: typeof confirm;
25
+ verifier: typeof verifier;
26
+ shared: typeof shared;
27
+ signed: typeof signed;
28
+ signature: typeof signature;
29
+ };
30
+ export default _default;
@@ -0,0 +1,134 @@
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 elliptic = require('elliptic');
40
+ const crypto_1 = __importDefault(require("crypto"));
41
+ const ed = __importStar(require("@noble/ed25519"));
42
+ const util_1 = require("../utils/util");
43
+ // ...
44
+ // Note: All functions expect parameters to be hex strings.
45
+ function pair_setup_aes_key(K) {
46
+ return crypto_1.default.createHash('sha512')
47
+ .update('Pair-Setup-AES-Key')
48
+ .update((0, util_1.hexString2ArrayBuffer)(K))
49
+ .digest('hex')
50
+ .substring(0, 32);
51
+ }
52
+ function pair_setup_aes_iv(K) {
53
+ let ab = crypto_1.default.createHash('sha512')
54
+ .update('Pair-Setup-AES-IV')
55
+ .update((0, util_1.hexString2ArrayBuffer)(K))
56
+ .digest();
57
+ ab = ab.slice(0, 16);
58
+ ab[ab.length - 1] += 0x01;
59
+ return (0, util_1.buf2hex)(ab);
60
+ }
61
+ function pair_verify_aes_key(shared) {
62
+ return (0, util_1.buf2hex)(crypto_1.default.createHash('sha512')
63
+ .update('Pair-Verify-AES-Key')
64
+ .update((0, util_1.hexString2ArrayBuffer)(shared))
65
+ .digest()
66
+ .slice(0, 16));
67
+ }
68
+ function pair_verify_aes_iv(shared) {
69
+ return (0, util_1.buf2hex)(crypto_1.default.createHash('sha512')
70
+ .update('Pair-Verify-AES-IV')
71
+ .update((0, util_1.hexString2ArrayBuffer)(shared))
72
+ .digest()
73
+ .slice(0, 16));
74
+ }
75
+ // ...
76
+ // Public.
77
+ function a_pub(a) {
78
+ return elliptic.utils.toHex(new elliptic.eddsa('ed25519').keyFromSecret(a).getPublic());
79
+ }
80
+ function confirm(a, K) {
81
+ const key = pair_setup_aes_key(K);
82
+ const iv = pair_setup_aes_iv(K);
83
+ const cipher = crypto_1.default.createCipheriv('aes-128-gcm', (0, util_1.hexString2ArrayBuffer)(key), (0, util_1.hexString2ArrayBuffer)(iv));
84
+ const encrypted = Buffer.concat([
85
+ cipher.update((0, util_1.hexString2ArrayBuffer)(a_pub(a))),
86
+ cipher.final(),
87
+ ]);
88
+ return {
89
+ epk: encrypted.toString('hex'),
90
+ authTag: (0, util_1.buf2hex)(cipher.getAuthTag()),
91
+ };
92
+ }
93
+ function verifier(a) {
94
+ const privateKey = Buffer.from(ed.utils.randomPrivateKey());
95
+ const publicKey = Buffer.from(ed.curve25519.scalarMultBase(privateKey));
96
+ const v_pri = (0, util_1.buf2hex)(privateKey);
97
+ const v_pub = (0, util_1.buf2hex)(publicKey);
98
+ const header = Buffer.from([0x01, 0x00, 0x00, 0x00]);
99
+ const a_pub_buf = Buffer.from(a_pub(a), 'hex');
100
+ return {
101
+ verifierBody: Buffer.concat([header, publicKey, a_pub_buf], header.byteLength + publicKey.byteLength + a_pub_buf.byteLength),
102
+ v_pri,
103
+ v_pub
104
+ };
105
+ }
106
+ function shared(v_pri, atv_pub) {
107
+ return (0, util_1.buf2hex)(Buffer.from(ed.curve25519.scalarMult((0, util_1.hexString2ArrayBuffer)(v_pri), (0, util_1.hexString2ArrayBuffer)(atv_pub))));
108
+ }
109
+ function signed(a, v_pub, atv_pub) {
110
+ const key = new elliptic.eddsa('ed25519').keyFromSecret(a);
111
+ return key.sign(v_pub + atv_pub).toHex();
112
+ }
113
+ function signature(shared, atv_data, signed) {
114
+ const cipher = crypto_1.default.createCipheriv('aes-128-ctr', (0, util_1.hexString2ArrayBuffer)(pair_verify_aes_key(shared)), (0, util_1.hexString2ArrayBuffer)(pair_verify_aes_iv(shared)));
115
+ // discard the result of encrypting atv_data.
116
+ cipher.update((0, util_1.hexString2ArrayBuffer)(atv_data));
117
+ const encrypted = Buffer.concat([
118
+ cipher.update(Buffer.from(signed, 'hex')),
119
+ cipher.final(),
120
+ ]);
121
+ return encrypted.toString('hex');
122
+ }
123
+ exports.default = {
124
+ pair_setup_aes_key,
125
+ pair_setup_aes_iv,
126
+ pair_verify_aes_key,
127
+ pair_verify_aes_iv,
128
+ a_pub,
129
+ confirm,
130
+ verifier,
131
+ shared,
132
+ signed,
133
+ signature,
134
+ };
@@ -0,0 +1,43 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type CircularBuffer from '../utils/circularBuffer';
3
+ type DevicesEmitter = EventEmitter & {
4
+ on(event: 'airtunes_devices', listener: (hasAirTunes: boolean) => void): DevicesEmitter;
5
+ on(event: 'need_sync', listener: () => void): DevicesEmitter;
6
+ emit(event: 'underrun'): boolean;
7
+ };
8
+ /**
9
+ * Generates RTP timestamps and sequence, pulling PCM/ALAC packets from a circular buffer.
10
+ * Emits `packet` events for devices and sync requests (`need_sync`) at intervals.
11
+ */
12
+ export default class AudioOut extends EventEmitter {
13
+ private lastSeq;
14
+ private lastWireSeq;
15
+ private hasAirTunes;
16
+ private rtpTimeRef;
17
+ private startTimeMs?;
18
+ private startTimeNtp?;
19
+ private latencyFrames;
20
+ private latencyApplied;
21
+ private seqOffset;
22
+ private tsOffset;
23
+ private syncOffsetFrames;
24
+ private deviceMagic;
25
+ private packetCount;
26
+ private octetCount;
27
+ private readonly frameDurationMs;
28
+ private muteUntilMs;
29
+ /**
30
+ * Begin pulling from the buffer and emitting packets at the configured cadence.
31
+ * @param devices Device manager for sync events.
32
+ * @param circularBuffer PCM/ALAC buffer.
33
+ * @param startTimeMs Optional unix ms to align playback.
34
+ * @param startTimeNtp Optional NTP uint64 (sec<<32|frac) to align playback.
35
+ */
36
+ init(devices: DevicesEmitter, circularBuffer: CircularBuffer, startTimeMs?: number, startTimeNtp?: bigint | number, deviceMagic?: number, underrunMuteMs?: number): void;
37
+ /**
38
+ * Apply latency (in audio frames) when aligning start time.
39
+ */
40
+ setLatencyFrames(latencyFrames: number): void;
41
+ private handleUnderrun;
42
+ }
43
+ export {};