appium-ios-remotexpc 0.3.0 → 0.3.1

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.3.1](https://github.com/appium/appium-ios-remotexpc/compare/v0.3.0...v0.3.1) (2025-08-31)
2
+
3
+ ### Miscellaneous Chores
4
+
5
+ * add bonjour service and AppleTV device info defaults ([#53](https://github.com/appium/appium-ios-remotexpc/issues/53)) ([a5924f8](https://github.com/appium/appium-ios-remotexpc/commit/a5924f8da8142dfd16219fe40f2421c528f534ce))
6
+
1
7
  ## [0.3.0](https://github.com/appium/appium-ios-remotexpc/compare/v0.2.0...v0.3.0) (2025-08-26)
2
8
 
3
9
  ### Features
@@ -0,0 +1,20 @@
1
+ import type { AppleTVDeviceInfo } from '../types.js';
2
+ type OpackSerialized = Buffer;
3
+ /**
4
+ * Creates a standardized Apple TV device information object with default values
5
+ * and the specified identifier as the account ID.
6
+ *
7
+ * @param identifier - Unique identifier to use as the account ID
8
+ * @returns Complete Apple TV device information object
9
+ */
10
+ export declare function createAppleTVDeviceInfo(identifier: string): AppleTVDeviceInfo;
11
+ /**
12
+ * Encodes Apple TV device information into a binary buffer using Opack2 serialization.
13
+ * This creates the device info object and immediately serializes it for transmission.
14
+ *
15
+ * @param identifier - Unique identifier to use as the account ID
16
+ * @returns Serialized device information as a Buffer
17
+ */
18
+ export declare function encodeAppleTVDeviceInfo(identifier: string): OpackSerialized;
19
+ export {};
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/deviceInfo/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,KAAK,eAAe,GAAG,MAAM,CAAC;AAU9B;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,CAU7E;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAM3E"}
@@ -0,0 +1,40 @@
1
+ import { hostname } from 'node:os';
2
+ import { Opack2 } from '../encryption/index.js';
3
+ const DEFAULT_ALT_IRK = Buffer.from([
4
+ 0xe9, 0xe8, 0x2d, 0xc0, 0x6a, 0x49, 0x79, 0x6b, 0x56, 0x6f, 0x54, 0x00, 0x19,
5
+ 0xb1, 0xc7, 0x7b,
6
+ ]);
7
+ const DEFAULT_BT_ADDR = '11:22:33:44:55:66';
8
+ const DEFAULT_MAC_BUFFER = Buffer.from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]);
9
+ const DEFAULT_PAIRING_SERIAL = 'AAAAAAAAAAAA';
10
+ /**
11
+ * Creates a standardized Apple TV device information object with default values
12
+ * and the specified identifier as the account ID.
13
+ *
14
+ * @param identifier - Unique identifier to use as the account ID
15
+ * @returns Complete Apple TV device information object
16
+ */
17
+ export function createAppleTVDeviceInfo(identifier) {
18
+ return {
19
+ altIRK: DEFAULT_ALT_IRK,
20
+ btAddr: DEFAULT_BT_ADDR,
21
+ mac: DEFAULT_MAC_BUFFER,
22
+ remotePairingSerialNumber: DEFAULT_PAIRING_SERIAL,
23
+ accountID: identifier,
24
+ model: 'computer-model',
25
+ name: hostname(),
26
+ };
27
+ }
28
+ /**
29
+ * Encodes Apple TV device information into a binary buffer using Opack2 serialization.
30
+ * This creates the device info object and immediately serializes it for transmission.
31
+ *
32
+ * @param identifier - Unique identifier to use as the account ID
33
+ * @returns Serialized device information as a Buffer
34
+ */
35
+ export function encodeAppleTVDeviceInfo(identifier) {
36
+ const deviceInfo = createAppleTVDeviceInfo(identifier);
37
+ // Cast to SerializableValue to ensure type compatibility with Opack2.dumps
38
+ // The AppleTVDeviceInfo structure is compatible with OPACK2 serialization
39
+ return Opack2.dumps(deviceInfo);
40
+ }
@@ -0,0 +1,9 @@
1
+ export type { AppleTVDeviceInfo, PairingKeys, PairingResult, PairingConfig, TLV8Item, PairingDataComponentTypeValue, Opack2Value, Opack2Array, Opack2Dictionary, } from './types.js';
2
+ export * from './errors.js';
3
+ export * from './constants.js';
4
+ export * from './utils/index.js';
5
+ export * from './deviceInfo/index.js';
6
+ export * from './encryption/index.js';
7
+ export * from './tlv/index.js';
8
+ export * from './srp/index.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/apple-tv/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,aAAa,EACb,QAAQ,EACR,6BAA6B,EAC7B,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export * from './errors.js';
2
+ export * from './constants.js';
3
+ export * from './utils/index.js';
4
+ export * from './deviceInfo/index.js';
5
+ export * from './encryption/index.js';
6
+ export * from './tlv/index.js';
7
+ export * from './srp/index.js';
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from 'node:events';
2
+ /**
3
+ * Interface for a discovered Bonjour service
4
+ */
5
+ export interface BonjourService {
6
+ name: string;
7
+ type: string;
8
+ domain: string;
9
+ hostname?: string;
10
+ port?: number;
11
+ txtRecord?: Record<string, string>;
12
+ interfaceIndex?: number;
13
+ }
14
+ /**
15
+ * Interface for AppleTV device discovered via Bonjour
16
+ */
17
+ export interface AppleTVDevice {
18
+ name: string;
19
+ identifier: string;
20
+ hostname: string;
21
+ ip?: string;
22
+ port: number;
23
+ model: string;
24
+ version: string;
25
+ minVersion: string;
26
+ authTag?: string;
27
+ interfaceIndex?: number;
28
+ }
29
+ /**
30
+ * Type alias for service discovery results
31
+ */
32
+ export type ServiceDiscoveryResult = Array<{
33
+ action: string;
34
+ service: BonjourService;
35
+ }>;
36
+ export declare class BonjourDiscovery extends EventEmitter {
37
+ private _browseProcess?;
38
+ private _isDiscovering;
39
+ private readonly _discoveredServices;
40
+ /**
41
+ * Start browsing for Bonjour services
42
+ */
43
+ startBrowsing(serviceType?: string, domain?: string): Promise<void>;
44
+ /**
45
+ * Stop browsing for services
46
+ */
47
+ stopBrowsing(): void;
48
+ /**
49
+ * Get all discovered services
50
+ */
51
+ getDiscoveredServices(): BonjourService[];
52
+ /**
53
+ * Resolve a specific service to get detailed information
54
+ */
55
+ resolveService(serviceName: string, serviceType?: string, domain?: string): Promise<BonjourService>;
56
+ /**
57
+ * Discover Apple TV devices with IP address resolution
58
+ */
59
+ discoverAppleTVDevicesWithIP(timeoutMs?: number): Promise<AppleTVDevice[]>;
60
+ /**
61
+ * Process browse output using the parser
62
+ */
63
+ processBrowseOutput(output: string): void;
64
+ /**
65
+ * Initialize a browsing process
66
+ */
67
+ private initializeBrowsing;
68
+ /**
69
+ * Setup event handlers for an ongoing browse process
70
+ */
71
+ private setupBrowseEventHandlers;
72
+ /**
73
+ * Resolve all discovered services
74
+ */
75
+ private resolveAllServices;
76
+ /**
77
+ * Cleanup resources
78
+ */
79
+ private cleanup;
80
+ }
81
+ //# sourceMappingURL=bonjour-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bonjour-discovery.d.ts","sourceRoot":"","sources":["../../../../src/lib/bonjour/bonjour-discovery.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiB3C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,KAAK,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,cAAc,CAAC;CACzB,CAAC,CAAC;AA6XH,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,OAAO,CAAC,cAAc,CAAC,CAAe;IACtC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA0C;IAE9E;;OAEG;IACG,aAAa,CACjB,WAAW,GAAE,MAA+C,EAC5D,MAAM,GAAE,MAA+B,GACtC,OAAO,CAAC,IAAI,CAAC;IAgBhB;;OAEG;IACH,YAAY,IAAI,IAAI;IAQpB;;OAEG;IACH,qBAAqB,IAAI,cAAc,EAAE;IAIzC;;OAEG;IACG,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,WAAW,GAAE,MAA+C,EAC5D,MAAM,GAAE,MAA+B,GACtC,OAAO,CAAC,cAAc,CAAC;IAoB1B;;OAEG;IACG,4BAA4B,CAChC,SAAS,GAAE,MAA2C,GACrD,OAAO,CAAC,aAAa,EAAE,CAAC;IAmB3B;;OAEG;IACH,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAsBzC;;OAEG;YACW,kBAAkB;IA4BhC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAuBhC;;OAEG;YACW,kBAAkB;IAuBhC;;OAEG;IACH,OAAO,CAAC,OAAO;CAMhB"}
@@ -0,0 +1,461 @@
1
+ import { logger } from '@appium/support';
2
+ import { spawn } from 'node:child_process';
3
+ import { resolve4 } from 'node:dns/promises';
4
+ import { EventEmitter } from 'node:events';
5
+ import { clearTimeout, setTimeout } from 'node:timers';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { BONJOUR_DEFAULT_DOMAIN, BONJOUR_SERVICE_TYPES, BONJOUR_TIMEOUTS, DNS_SD_ACTIONS, DNS_SD_COMMANDS, DNS_SD_PATTERNS, } from './constants.js';
8
+ const log = logger.getLogger('BonjourDiscovery');
9
+ const DNS_SD_COMMAND = 'dns-sd';
10
+ /**
11
+ * Execute a dns-sd command with a timeout.
12
+ *
13
+ * @param args Arguments passed to the dns-sd CLI.
14
+ * @param timeoutMs Timeout in milliseconds.
15
+ * @param outputHandler Called for each stdout chunk from dns-sd:
16
+ * - returns true: chunk handled; no result recorded; continue listening
17
+ * - returns a truthy non-true value (e.g., object): store as result to return when the process ends
18
+ * - returns false/undefined/null: ignore; continue listening
19
+ * - throws: mark command as failed
20
+ * @param timeoutMessage Error message used if the command times out.
21
+ * @returns The value produced by outputHandler, if any, when the process ends.
22
+ */
23
+ async function executeDnsSdCommand(args, timeoutMs, outputHandler, timeoutMessage) {
24
+ const child = spawn(DNS_SD_COMMAND, args);
25
+ try {
26
+ const result = await waitForProcessResult(child, timeoutMs, outputHandler, timeoutMessage);
27
+ if (!result.success) {
28
+ throw result.error || new Error('Process execution failed');
29
+ }
30
+ return result.data;
31
+ }
32
+ finally {
33
+ if (!child.killed) {
34
+ child.kill('SIGTERM');
35
+ }
36
+ }
37
+ }
38
+ /**
39
+ * Create a long-running browse process
40
+ */
41
+ function createDnsSdBrowseProcess(serviceType, domain) {
42
+ return spawn(DNS_SD_COMMAND, [DNS_SD_COMMANDS.BROWSE, serviceType, domain]);
43
+ }
44
+ /**
45
+ * Wait for a child process result with a timeout.
46
+ *
47
+ * @param process Child process to observe.
48
+ * @param timeoutMs Timeout in milliseconds.
49
+ * @param outputHandler Called for each stdout chunk from dns-sd:
50
+ * - returns true: chunk handled; no result recorded; continue listening
51
+ * - returns a truthy non-true value (e.g., object): store as result to return when the process ends
52
+ * - returns false/undefined/null: ignore; continue listening
53
+ * - throws: mark command as failed
54
+ * @param timeoutMessage Error message used if the operation times out.
55
+ * @returns A ProcessResult indicating success and any data from outputHandler.
56
+ */
57
+ async function waitForProcessResult(process, timeoutMs, outputHandler, timeoutMessage) {
58
+ return new Promise((resolve, reject) => {
59
+ let isResolved = false;
60
+ let result;
61
+ let exitCode = null;
62
+ let hasError = false;
63
+ let errorMessage = '';
64
+ const timeout = setTimeout(() => {
65
+ if (!isResolved) {
66
+ isResolved = true;
67
+ reject(new Error(timeoutMessage));
68
+ }
69
+ }, timeoutMs);
70
+ const cleanup = () => {
71
+ clearTimeout(timeout);
72
+ if (!isResolved) {
73
+ isResolved = true;
74
+ if (hasError) {
75
+ reject(new Error(errorMessage));
76
+ }
77
+ else if (exitCode !== null && exitCode !== 0) {
78
+ reject(new Error(`Process exited with code ${exitCode}`));
79
+ }
80
+ else if (result !== undefined) {
81
+ resolve({ success: true, data: result });
82
+ }
83
+ else {
84
+ resolve({ success: true });
85
+ }
86
+ }
87
+ };
88
+ process.stdout?.on('data', (data) => {
89
+ if (isResolved) {
90
+ return;
91
+ }
92
+ const output = data.toString();
93
+ log.debug(`[dns-sd] output: ${output}`);
94
+ try {
95
+ const handlerResult = outputHandler(output);
96
+ if (handlerResult === true) {
97
+ result = undefined;
98
+ }
99
+ else if (handlerResult) {
100
+ result = handlerResult;
101
+ }
102
+ }
103
+ catch (error) {
104
+ hasError = true;
105
+ errorMessage = `Output handler error: ${error}`;
106
+ }
107
+ });
108
+ process.stderr?.on('data', (data) => {
109
+ if (isResolved) {
110
+ return;
111
+ }
112
+ const error = data.toString();
113
+ log.error(`[dns-sd] error: ${error}`);
114
+ hasError = true;
115
+ errorMessage = `Process failed: ${error}`;
116
+ });
117
+ process.on('error', (error) => {
118
+ if (isResolved) {
119
+ return;
120
+ }
121
+ log.error(`[dns-sd] failed to start process: ${error}`);
122
+ hasError = true;
123
+ errorMessage = `Failed to start process: ${error}`;
124
+ });
125
+ process.on('exit', (code) => {
126
+ exitCode = code;
127
+ if (code !== null && code !== 0) {
128
+ log.error(`[dns-sd] process exited with error code: ${code}`);
129
+ }
130
+ });
131
+ process.on('close', (code) => {
132
+ log.debug(`[dns-sd] process closed with code: ${code}`);
133
+ cleanup();
134
+ });
135
+ });
136
+ }
137
+ /**
138
+ * Parse browse output and extract service information
139
+ */
140
+ function parseBrowseOutput(output) {
141
+ const reducer = (acc, line) => {
142
+ const match = line.match(DNS_SD_PATTERNS.BROWSE_LINE);
143
+ if (!match) {
144
+ return acc;
145
+ }
146
+ const [, , action, , interfaceIndex, domain, serviceType, name] = match;
147
+ const trimmedName = name.trim();
148
+ const service = {
149
+ name: trimmedName,
150
+ type: serviceType,
151
+ domain,
152
+ interfaceIndex: parseInt(interfaceIndex, 10),
153
+ };
154
+ acc.push({ action, service });
155
+ return acc;
156
+ };
157
+ return output
158
+ .split('\n')
159
+ .filter((line) => !shouldSkipLine(line))
160
+ .reduce(reducer, []);
161
+ }
162
+ /**
163
+ * Parse resolve output and extract service details
164
+ */
165
+ function parseResolveOutput(output, serviceName, serviceType, domain) {
166
+ return parseOutput(output, (line, result) => {
167
+ // If we already found a result, return it (early termination)
168
+ if (result) {
169
+ return result;
170
+ }
171
+ const reachableMatch = line.match(DNS_SD_PATTERNS.REACHABLE);
172
+ if (reachableMatch) {
173
+ const [, hostname, port, interfaceIndex] = reachableMatch;
174
+ const txtRecord = parseTxtRecord(output);
175
+ return {
176
+ name: serviceName,
177
+ type: serviceType,
178
+ domain,
179
+ hostname,
180
+ port: parseInt(port, 10),
181
+ txtRecord,
182
+ interfaceIndex: parseInt(interfaceIndex, 10),
183
+ };
184
+ }
185
+ return result;
186
+ }, null);
187
+ }
188
+ /**
189
+ * Generic method to parse output with different reducer functions
190
+ */
191
+ function parseOutput(output, reducer, initialValue) {
192
+ const lines = output.split('\n');
193
+ let result = initialValue;
194
+ for (const line of lines) {
195
+ if (shouldSkipLine(line)) {
196
+ continue;
197
+ }
198
+ result = reducer(line, result);
199
+ }
200
+ return result;
201
+ }
202
+ /**
203
+ * Parse TXT record from output
204
+ */
205
+ function parseTxtRecord(output) {
206
+ const txtRecord = {};
207
+ const txtMatch = output.match(DNS_SD_PATTERNS.TXT_RECORD);
208
+ if (txtMatch) {
209
+ const [, identifier, authTag, model, name, ver, minVer] = txtMatch;
210
+ txtRecord.identifier = identifier;
211
+ txtRecord.authTag = authTag;
212
+ txtRecord.model = model;
213
+ txtRecord.name = name;
214
+ txtRecord.ver = ver;
215
+ txtRecord.minVer = minVer;
216
+ }
217
+ return txtRecord;
218
+ }
219
+ /**
220
+ * Check if line should be skipped
221
+ */
222
+ function shouldSkipLine(line) {
223
+ return (line.includes('Timestamp') || line.includes('---') || line.trim() === '');
224
+ }
225
+ /**
226
+ * Resolve hostname to IP address
227
+ */
228
+ async function resolveIPAddress(hostname) {
229
+ try {
230
+ const address = await resolve4(hostname);
231
+ log.info(`[ServiceResolver] Resolved ${hostname} to IPv4: ${address}`);
232
+ return address;
233
+ }
234
+ catch (error) {
235
+ log.warn(`[ServiceResolver] Failed to resolve hostname ${hostname} to IPv4: ${error}`);
236
+ // For .local hostnames, try without the trailing dot
237
+ if (hostname.endsWith('.local.')) {
238
+ const cleanHostname = hostname.slice(0, -1); // Remove trailing dot
239
+ try {
240
+ const address = await resolve4(cleanHostname);
241
+ log.info(`[ServiceResolver] Resolved ${cleanHostname} to IPv4: ${address}`);
242
+ return address;
243
+ }
244
+ catch (retryError) {
245
+ log.warn(`[ServiceResolver] Failed to resolve ${cleanHostname} to IPv4: ${retryError}`);
246
+ }
247
+ }
248
+ return undefined;
249
+ }
250
+ }
251
+ /**
252
+ * Convert a resolved Bonjour service to an Apple TV device with IP resolution
253
+ */
254
+ async function convertToAppleTVDeviceWithIP(service) {
255
+ if (!isValidService(service)) {
256
+ return null;
257
+ }
258
+ const { txtRecord, hostname, port } = service;
259
+ if (!txtRecord || !hasRequiredTxtFields(txtRecord)) {
260
+ log.warn(`[AppleTVDeviceConverter] Service ${service.name} missing required TXT record fields`);
261
+ return null;
262
+ }
263
+ if (!hostname || !port) {
264
+ log.warn(`[AppleTVDeviceConverter] Service ${service.name} missing hostname or port`);
265
+ return null;
266
+ }
267
+ const ipAddresses = await resolveIPAddress(hostname);
268
+ // Select default first one
269
+ // TODO: needs a decision to select from cli, if the user wants to select from the available ip's
270
+ const ip = ipAddresses?.[0];
271
+ return {
272
+ name: service.name,
273
+ identifier: txtRecord.identifier,
274
+ hostname,
275
+ ip,
276
+ port,
277
+ model: txtRecord.model,
278
+ version: txtRecord.ver,
279
+ minVersion: txtRecord.minVer || '17',
280
+ authTag: txtRecord.authTag,
281
+ interfaceIndex: service.interfaceIndex,
282
+ };
283
+ }
284
+ /**
285
+ * Check if the service has required fields
286
+ */
287
+ function isValidService(service) {
288
+ return Boolean(service.hostname && service.port && service.txtRecord);
289
+ }
290
+ /**
291
+ * Check if TXT record has required fields
292
+ */
293
+ function hasRequiredTxtFields(txtRecord) {
294
+ return Boolean(txtRecord.identifier && txtRecord.model && txtRecord.ver);
295
+ }
296
+ /* =========================
297
+ * Main Bonjour discovery service orchestrator
298
+ * =========================
299
+ */
300
+ export class BonjourDiscovery extends EventEmitter {
301
+ _browseProcess;
302
+ _isDiscovering = false;
303
+ _discoveredServices = new Map();
304
+ /**
305
+ * Start browsing for Bonjour services
306
+ */
307
+ async startBrowsing(serviceType = BONJOUR_SERVICE_TYPES.APPLE_TV_PAIRING, domain = BONJOUR_DEFAULT_DOMAIN) {
308
+ if (this._isDiscovering) {
309
+ log.warn('Already discovering services');
310
+ return;
311
+ }
312
+ log.info(`Starting Bonjour discovery for ${serviceType}.${domain}`);
313
+ try {
314
+ await this.initializeBrowsing(serviceType, domain);
315
+ }
316
+ catch (error) {
317
+ this.cleanup();
318
+ throw error;
319
+ }
320
+ }
321
+ /**
322
+ * Stop browsing for services
323
+ */
324
+ stopBrowsing() {
325
+ if (this._browseProcess && !this._browseProcess.killed) {
326
+ log.info('Stopping Bonjour discovery');
327
+ this._browseProcess.kill('SIGTERM');
328
+ }
329
+ this.cleanup();
330
+ }
331
+ /**
332
+ * Get all discovered services
333
+ */
334
+ getDiscoveredServices() {
335
+ return Array.from(this._discoveredServices.values());
336
+ }
337
+ /**
338
+ * Resolve a specific service to get detailed information
339
+ */
340
+ async resolveService(serviceName, serviceType = BONJOUR_SERVICE_TYPES.APPLE_TV_PAIRING, domain = BONJOUR_DEFAULT_DOMAIN) {
341
+ log.info(`[ServiceResolver] Resolving service: ${serviceName}.${serviceType}.${domain}`);
342
+ const service = await executeDnsSdCommand([DNS_SD_COMMANDS.RESOLVE, serviceName, serviceType, domain], BONJOUR_TIMEOUTS.SERVICE_RESOLUTION, (output) => parseResolveOutput(output, serviceName, serviceType, domain), `Service resolution timeout for ${serviceName}`);
343
+ if (!service) {
344
+ throw new Error(`Failed to resolve service ${serviceName}`);
345
+ }
346
+ return service;
347
+ }
348
+ /**
349
+ * Discover Apple TV devices with IP address resolution
350
+ */
351
+ async discoverAppleTVDevicesWithIP(timeoutMs = BONJOUR_TIMEOUTS.DEFAULT_DISCOVERY) {
352
+ log.info('Starting Apple TV device discovery with IP resolution');
353
+ try {
354
+ await this.startBrowsing();
355
+ await delay(timeoutMs);
356
+ const devices = await this.resolveAllServices();
357
+ log.info(`Discovered ${devices.length} Apple TV device(s) with IP addresses:`, devices);
358
+ return devices;
359
+ }
360
+ finally {
361
+ this.stopBrowsing();
362
+ }
363
+ }
364
+ /**
365
+ * Process browse output using the parser
366
+ */
367
+ processBrowseOutput(output) {
368
+ const results = parseBrowseOutput(output);
369
+ for (const { action, service } of results) {
370
+ switch (action) {
371
+ case DNS_SD_ACTIONS.ADD:
372
+ this._discoveredServices.set(service.name, service);
373
+ this.emit('serviceAdded', service);
374
+ log.info(`Discovered service: ${service.name}`);
375
+ break;
376
+ case DNS_SD_ACTIONS.REMOVE:
377
+ this._discoveredServices.delete(service.name);
378
+ this.emit('serviceRemoved', service.name);
379
+ log.info(`Service removed: ${service.name}`);
380
+ break;
381
+ default:
382
+ log.debug(`Unknown action: ${action}`);
383
+ break;
384
+ }
385
+ }
386
+ }
387
+ /**
388
+ * Initialize a browsing process
389
+ */
390
+ async initializeBrowsing(serviceType, domain) {
391
+ this._isDiscovering = true;
392
+ this._discoveredServices.clear();
393
+ const browseProcess = createDnsSdBrowseProcess(serviceType, domain);
394
+ this._browseProcess = browseProcess;
395
+ try {
396
+ await executeDnsSdCommand([DNS_SD_COMMANDS.BROWSE, serviceType, domain], BONJOUR_TIMEOUTS.BROWSE_STARTUP, (output) => {
397
+ this.processBrowseOutput(output);
398
+ return output.includes(DNS_SD_PATTERNS.STARTING);
399
+ }, 'DNS-SD browse startup timeout');
400
+ this.setupBrowseEventHandlers(browseProcess);
401
+ }
402
+ catch (error) {
403
+ this._isDiscovering = false;
404
+ throw error;
405
+ }
406
+ }
407
+ /**
408
+ * Setup event handlers for an ongoing browse process
409
+ */
410
+ setupBrowseEventHandlers(process) {
411
+ process.stdout?.on('data', (data) => {
412
+ const output = data.toString();
413
+ log.debug(`dns-sd browse output: ${output}`);
414
+ this.processBrowseOutput(output);
415
+ });
416
+ process.stderr?.on('data', (data) => {
417
+ const error = data.toString();
418
+ log.error(`dns-sd browse error: ${error}`);
419
+ });
420
+ process.on('exit', (code) => {
421
+ if (code !== null && code !== 0) {
422
+ log.error(`dns-sd browse process exited with error code: ${code}`);
423
+ }
424
+ });
425
+ process.on('close', (code) => {
426
+ log.debug(`dns-sd browse process closed with code: ${code}`);
427
+ this.cleanup();
428
+ });
429
+ }
430
+ /**
431
+ * Resolve all discovered services
432
+ */
433
+ async resolveAllServices() {
434
+ const services = this.getDiscoveredServices();
435
+ log.info(`Found ${services.length} services to resolve`);
436
+ const devices = [];
437
+ for (const service of services) {
438
+ try {
439
+ log.info(`Attempting to resolve service: ${service.name}`);
440
+ const resolvedService = await this.resolveService(service.name);
441
+ const device = await convertToAppleTVDeviceWithIP(resolvedService);
442
+ if (device) {
443
+ devices.push(device);
444
+ }
445
+ }
446
+ catch (error) {
447
+ log.warn(`Failed to resolve service ${service.name}: ${error}`);
448
+ }
449
+ }
450
+ return devices;
451
+ }
452
+ /**
453
+ * Cleanup resources
454
+ */
455
+ cleanup() {
456
+ log.debug('Cleaning up BonjourDiscovery resources');
457
+ this._browseProcess = undefined;
458
+ this._isDiscovering = false;
459
+ this._discoveredServices.clear();
460
+ }
461
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Constants for Bonjour discovery
3
+ */
4
+ export declare const BONJOUR_TIMEOUTS: {
5
+ readonly BROWSE_STARTUP: 5000;
6
+ readonly SERVICE_RESOLUTION: 10000;
7
+ readonly DEFAULT_DISCOVERY: 5000;
8
+ };
9
+ export declare const BONJOUR_SERVICE_TYPES: {
10
+ readonly APPLE_TV_PAIRING: "_remotepairing-manual-pairing._tcp";
11
+ };
12
+ export declare const BONJOUR_DEFAULT_DOMAIN = "local";
13
+ export declare const DNS_SD_COMMANDS: {
14
+ readonly BROWSE: "-B";
15
+ readonly RESOLVE: "-L";
16
+ };
17
+ export declare const DNS_SD_ACTIONS: {
18
+ readonly ADD: "Add";
19
+ readonly REMOVE: "Rmv";
20
+ };
21
+ export declare const DNS_SD_PATTERNS: {
22
+ readonly STARTING: "...STARTING...";
23
+ readonly BROWSE_LINE: RegExp;
24
+ readonly REACHABLE: RegExp;
25
+ readonly TXT_RECORD: RegExp;
26
+ };
27
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/lib/bonjour/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,eAAO,MAAM,gBAAgB;;;;CAInB,CAAC;AAGX,eAAO,MAAM,qBAAqB;;CAExB,CAAC;AAEX,eAAO,MAAM,sBAAsB,UAAU,CAAC;AAG9C,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAGX,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAGX,eAAO,MAAM,eAAe;;;;;CAOlB,CAAC"}