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,209 @@
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 async_1 = __importDefault(require("async"));
8
+ const deviceAirtunes_1 = __importDefault(require("./deviceAirtunes"));
9
+ /**
10
+ * Tracks and controls all connected AirPlay/RAOP devices.
11
+ * Responsible for lifecycle, fan-out of state, and sync signaling.
12
+ */
13
+ class Devices extends node_events_1.EventEmitter {
14
+ audioOut;
15
+ devices = {};
16
+ hasAirTunes = false;
17
+ constructor(audioOut) {
18
+ super();
19
+ this.audioOut = audioOut;
20
+ }
21
+ /** Wire device sync events from AudioOut into individual devices. */
22
+ init() {
23
+ this.audioOut.on('need_sync', (seq) => {
24
+ this.forEach((dev) => {
25
+ try {
26
+ if (dev.onSyncNeeded && dev.controlPort) {
27
+ dev.onSyncNeeded(seq);
28
+ }
29
+ }
30
+ catch {
31
+ // ignore
32
+ }
33
+ });
34
+ });
35
+ }
36
+ /** Iterate over live devices. */
37
+ forEach(it) {
38
+ for (const key of Object.keys(this.devices)) {
39
+ it(this.devices[key], key);
40
+ }
41
+ }
42
+ /**
43
+ * Add (or reuse) a device and start playback.
44
+ * @param type Device type (airtunes/coreaudio).
45
+ * @param host Target host/IP.
46
+ * @param options Transport options (volume/password/etc).
47
+ * @param mode RAOP mode (0 default, 2 for AirPlay 2).
48
+ * @param txt TXT records advertised by the device.
49
+ */
50
+ add(type, host, options, mode = 0, txt = '') {
51
+ this.emit('status', host ?? 'unknown', 'connecting', '');
52
+ const dev = new deviceAirtunes_1.default(host, this.audioOut, options, mode, txt);
53
+ const previousDev = this.devices[dev.key];
54
+ if (previousDev) {
55
+ previousDev.reportStatus();
56
+ return previousDev;
57
+ }
58
+ this.devices[dev.key] = dev;
59
+ dev.on('status', (status) => {
60
+ if (status === 'error' || status === 'stopped') {
61
+ delete this.devices[dev.key];
62
+ this.checkAirTunesDevices();
63
+ }
64
+ if (this.hasAirTunes && status === 'playing') {
65
+ this.emit('need_sync');
66
+ }
67
+ this.emit('status', dev.key, status, '');
68
+ });
69
+ dev.start();
70
+ this.checkAirTunesDevices();
71
+ return dev;
72
+ }
73
+ /** Adjust volume on one device. */
74
+ setVolume(key, volume, callback) {
75
+ const dev = this.devices[key];
76
+ if (!dev) {
77
+ this.emit('status', key, 'error', 'not_found');
78
+ return;
79
+ }
80
+ dev.setVolume(volume, callback);
81
+ }
82
+ /** Push playback position to one or all devices. */
83
+ setProgress(key, progress, duration, callback) {
84
+ try {
85
+ if (key === 'all') {
86
+ for (const device of Object.keys(this.devices)) {
87
+ try {
88
+ this.devices[device].setProgress(progress, duration, callback);
89
+ }
90
+ catch (err) {
91
+ if (err?.name === 'TypeError') {
92
+ delete this.devices[device];
93
+ }
94
+ }
95
+ }
96
+ return;
97
+ }
98
+ const dev = this.devices[key];
99
+ if (!dev) {
100
+ this.emit('status', key, 'error', 'not_found');
101
+ return;
102
+ }
103
+ dev.setProgress(progress, duration, callback);
104
+ }
105
+ catch {
106
+ // ignore
107
+ }
108
+ }
109
+ /**
110
+ * Update track info on one or all devices.
111
+ */
112
+ setTrackInfo(key, name, artist, album, callback) {
113
+ try {
114
+ if (key === 'all') {
115
+ for (const device of Object.keys(this.devices)) {
116
+ try {
117
+ this.devices[device].setTrackInfo(name, artist, album, callback);
118
+ }
119
+ catch (err) {
120
+ if (err?.name === 'TypeError') {
121
+ delete this.devices[device];
122
+ }
123
+ }
124
+ }
125
+ return;
126
+ }
127
+ const dev = this.devices[key];
128
+ if (!dev) {
129
+ this.emit('status', key, 'error', 'not_found');
130
+ return;
131
+ }
132
+ dev.setTrackInfo(name, artist, album, callback);
133
+ }
134
+ catch {
135
+ // ignore
136
+ }
137
+ }
138
+ /** Update artwork on one or all devices. */
139
+ setArtwork(key, art, contentType, callback) {
140
+ try {
141
+ if (key === 'all') {
142
+ for (const device of Object.keys(this.devices)) {
143
+ try {
144
+ this.devices[device].setArtwork(art, contentType, callback);
145
+ }
146
+ catch (err) {
147
+ if (err?.name === 'TypeError') {
148
+ delete this.devices[device];
149
+ }
150
+ }
151
+ }
152
+ return;
153
+ }
154
+ const dev = this.devices[key];
155
+ if (!dev) {
156
+ this.emit('status', key, 'error', 'not_found');
157
+ return;
158
+ }
159
+ dev.setArtwork(art, contentType, callback);
160
+ }
161
+ catch {
162
+ // ignore
163
+ }
164
+ }
165
+ /** Provide a passcode to a specific device. */
166
+ setPasscode(key, passcode) {
167
+ const dev = this.devices[key];
168
+ if (!dev) {
169
+ this.emit('status', key, 'error', 'not_found');
170
+ return;
171
+ }
172
+ dev.setPasscode(passcode);
173
+ }
174
+ /** Stop one device. */
175
+ stop(key) {
176
+ const dev = this.devices[key];
177
+ if (!dev) {
178
+ this.emit('status', key, 'error', 'not_found');
179
+ return;
180
+ }
181
+ dev.stop();
182
+ delete this.devices[key];
183
+ }
184
+ /** Stop every device in parallel. */
185
+ stopAll(allCb) {
186
+ const devices = Object.values(this.devices);
187
+ async_1.default.each(devices, (dev, callback) => {
188
+ dev.stop(callback);
189
+ }, (err) => {
190
+ if (allCb) {
191
+ allCb(err);
192
+ }
193
+ });
194
+ }
195
+ /** Track whether any active device is RAOP to drive sync signals. */
196
+ checkAirTunesDevices() {
197
+ let hasAirTunes = false;
198
+ this.forEach((dev) => {
199
+ if (dev.type === 'airtunes') {
200
+ hasAirTunes = true;
201
+ }
202
+ });
203
+ if (hasAirTunes !== this.hasAirTunes) {
204
+ this.hasAirTunes = hasAirTunes;
205
+ this.emit('airtunes_devices', hasAirTunes);
206
+ }
207
+ }
208
+ }
209
+ exports.default = Devices;
@@ -0,0 +1,47 @@
1
+ import { Duplex } from 'node:stream';
2
+ import Devices from './devices';
3
+ /**
4
+ * High-level RAOP/AirPlay sender that wires together devices, buffering, and output.
5
+ * Acts as a Duplex stream: write PCM/ALAC chunks, listen to status events.
6
+ */
7
+ declare class AirTunes extends Duplex {
8
+ readonly devices: Devices;
9
+ private readonly circularBuffer;
10
+ /**
11
+ * @param options.packetSize Override packet size; defaults to config.
12
+ * @param options.startTimeMs Optional unix ms to align playback start.
13
+ */
14
+ constructor(options?: {
15
+ packetSize?: number;
16
+ startTimeMs?: number;
17
+ });
18
+ /** Register an AirTunes (RAOP) device and start streaming to it. */
19
+ add(host: string, options: Record<string, unknown>, mode?: number, txt?: string[] | string): any;
20
+ /** Register a CoreAudio output (legacy shim). */
21
+ addCoreAudio(options: Record<string, unknown>): unknown;
22
+ /** Stop every device and release resources. */
23
+ stopAll(cb?: () => void): void;
24
+ /** Stop a single device by key. */
25
+ stop(deviceKey: string): void;
26
+ /** Adjust volume for a device. */
27
+ setVolume(deviceKey: string, volume: number, callback?: (err?: unknown) => void): void;
28
+ /**
29
+ * Push playback position (seconds) to a device.
30
+ */
31
+ setProgress(deviceKey: string, progress: number, duration: number, callback?: (err?: unknown) => void): void;
32
+ /**
33
+ * Update track title/artist/album on a device.
34
+ */
35
+ setTrackInfo(deviceKey: string, name: string, artist?: string, album?: string, callback?: (err?: unknown) => void): void;
36
+ /** Reset the circular buffer state. */
37
+ reset(): void;
38
+ /** Send artwork to a device. */
39
+ setArtwork(deviceKey: string, art: Buffer, contentType?: string, callback?: (err?: unknown) => void): void;
40
+ /** Write PCM/ALAC frames into the buffer. */
41
+ write(data: Buffer): boolean;
42
+ /** Provide a passcode to a device requiring auth. */
43
+ setPasscode(deviceKey: string, passcode: string): void;
44
+ /** Close the writable side and stop buffering. */
45
+ end(chunk?: any, encoding?: any, cb?: any): this;
46
+ }
47
+ export default AirTunes;
@@ -0,0 +1,97 @@
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_stream_1 = require("node:stream");
7
+ const devices_1 = __importDefault(require("./devices"));
8
+ const config_1 = __importDefault(require("../utils/config"));
9
+ const circularBuffer_1 = __importDefault(require("../utils/circularBuffer"));
10
+ const audioOut_1 = __importDefault(require("./audioOut"));
11
+ /**
12
+ * High-level RAOP/AirPlay sender that wires together devices, buffering, and output.
13
+ * Acts as a Duplex stream: write PCM/ALAC chunks, listen to status events.
14
+ */
15
+ class AirTunes extends node_stream_1.Duplex {
16
+ devices;
17
+ circularBuffer;
18
+ /**
19
+ * @param options.packetSize Override packet size; defaults to config.
20
+ * @param options.startTimeMs Optional unix ms to align playback start.
21
+ */
22
+ constructor(options = {}) {
23
+ super({ readableObjectMode: false, writableObjectMode: false });
24
+ const audioOut = new audioOut_1.default();
25
+ this.devices = new devices_1.default(audioOut);
26
+ this.devices.init();
27
+ this.devices.on('status', (key, status, desc) => {
28
+ this.emit('device', key, status, desc);
29
+ });
30
+ const packetSize = options.packetSize ?? config_1.default.packet_size;
31
+ this.circularBuffer = new circularBuffer_1.default(config_1.default.packets_in_buffer, packetSize);
32
+ this.circularBuffer.on('status', (status) => {
33
+ this.emit('buffer', status);
34
+ });
35
+ audioOut.init(this.devices, this.circularBuffer, options.startTimeMs);
36
+ this.circularBuffer.on('drain', () => {
37
+ this.emit('drain');
38
+ });
39
+ this.circularBuffer.on('error', (err) => {
40
+ this.emit('error', err);
41
+ });
42
+ }
43
+ /** Register an AirTunes (RAOP) device and start streaming to it. */
44
+ add(host, options, mode = 0, txt = '') {
45
+ return this.devices.add('airtunes', host, options, mode, txt);
46
+ }
47
+ /** Register a CoreAudio output (legacy shim). */
48
+ addCoreAudio(options) {
49
+ return this.devices.add('coreaudio', null, options);
50
+ }
51
+ /** Stop every device and release resources. */
52
+ stopAll(cb) {
53
+ this.devices.stopAll(cb);
54
+ }
55
+ /** Stop a single device by key. */
56
+ stop(deviceKey) {
57
+ this.devices.stop(deviceKey);
58
+ }
59
+ /** Adjust volume for a device. */
60
+ setVolume(deviceKey, volume, callback) {
61
+ this.devices.setVolume(deviceKey, volume, callback);
62
+ }
63
+ /**
64
+ * Push playback position (seconds) to a device.
65
+ */
66
+ setProgress(deviceKey, progress, duration, callback) {
67
+ this.devices.setProgress(deviceKey, progress, duration, callback);
68
+ }
69
+ /**
70
+ * Update track title/artist/album on a device.
71
+ */
72
+ setTrackInfo(deviceKey, name, artist, album, callback) {
73
+ this.devices.setTrackInfo(deviceKey, name, artist, album, callback);
74
+ }
75
+ /** Reset the circular buffer state. */
76
+ reset() {
77
+ this.circularBuffer.reset();
78
+ }
79
+ /** Send artwork to a device. */
80
+ setArtwork(deviceKey, art, contentType, callback) {
81
+ this.devices.setArtwork(deviceKey, art, contentType, callback);
82
+ }
83
+ /** Write PCM/ALAC frames into the buffer. */
84
+ write(data) {
85
+ return this.circularBuffer.write(data);
86
+ }
87
+ /** Provide a passcode to a device requiring auth. */
88
+ setPasscode(deviceKey, passcode) {
89
+ this.devices.setPasscode(deviceKey, passcode);
90
+ }
91
+ /** Close the writable side and stop buffering. */
92
+ end(chunk, encoding, cb) {
93
+ this.circularBuffer.end();
94
+ return super.end(chunk, encoding, cb);
95
+ }
96
+ }
97
+ exports.default = AirTunes;
@@ -0,0 +1,12 @@
1
+ type AnyObject = Record<string, any>;
2
+ type LogFn = (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: unknown) => void;
3
+ type ClientInstance = AnyObject & {
4
+ log?: LogFn;
5
+ logLine?: (...args: any[]) => void;
6
+ };
7
+ declare function Client(this: ClientInstance, volume: number, password: string | null, audioOut: any, options: AnyObject): void;
8
+ export { Client };
9
+ declare const _default: {
10
+ Client: typeof Client;
11
+ };
12
+ export default _default;