scanMdnsDevices function
Future<List<DiscoveredDevice> >
scanMdnsDevices({})
共用 mDNS 掃描器
Implementation
Future<List<DiscoveredDevice>> scanMdnsDevices({
required String
serviceType, // 例如 '_airplay._tcp', '_raop._tcp', '_googlecast._tcp'
required String
mdnsType, // 例如 '_airplay._tcp', '_raop._tcp', '_googlecast._tcp'
required DiscoveredDevice Function({
required String ip,
required int port,
required String serviceName,
required Map<String, String> txtMap,
List<String>? mdnsTypes,
})
deviceFactory,
Function(DiscoveredDevice)? onDeviceFound,
String? logTag,
Duration sendQueryMessageInterval = const Duration(seconds: 3),
Duration scanDuration = const Duration(seconds: 15), // 改名為 scanDuration
bool useSystemMdns = false, // 新增參數,預設 false
}) async {
final logger = AppLogger();
final List<DiscoveredDevice> devices = [];
final deviceMap = <String, DiscoveredDevice>{};
MDnsClient? client;
logTag ??= 'mDNS';
// 平台判斷
bool isApplePlatform = false;
try {
isApplePlatform = Platform.isIOS || Platform.isMacOS;
} catch (_) {}
if (useSystemMdns && isApplePlatform) {
// TODO: 實作系統內建 mDNS 查詢
await logger.info('info.use_system_mdns', tag: logTag);
throw UnimplementedError('系統內建 mDNS 尚未實作,請自行擴充');
}
try {
client = MDnsClient(
rawDatagramSocketFactory: (
dynamic host,
int port, {
bool? reuseAddress,
bool? reusePort,
int? ttl,
}) {
return _safeBind(host, port, ttl: ttl);
},
);
await client.start();
await logger.info('info.standard_mdns_port', tag: logTag);
} on SocketException catch (e) {
if (e.osError?.errorCode == 48 || e.osError?.errorCode == 10048) {
await logger.error(
'errors.port_in_use',
tag: logTag,
params: {'port': e.port},
);
throw MdnsPortInUseException(
'mDNS port e.port} is in use, cannot scan for $serviceType devices',
);
}
rethrow;
} catch (e) {
await logger.error('errors.mdns_init_error', tag: logTag, error: e);
rethrow;
}
final completer = Completer<void>();
final List<StreamSubscription> subscriptions = [];
Timer? periodicQueryTimer;
bool isCompleted = false;
Future<void> sendPtrQuery() async {
final ptrStream = client!.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer('$serviceType.local'),
);
final ptrSubscription = ptrStream.listen((ptr) async {
final serviceName = ptr.domainName;
await for (final srv in client!.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(serviceName),
)) {
await logger.debug(
'debug.found_service',
tag: logTag,
params: {
'target': srv.target,
'port': srv.port,
'priority': srv.priority,
'weight': srv.weight,
},
);
await for (final ip in client.lookup<IPAddressResourceRecord>(
ResourceRecordQuery.addressIPv4(srv.target),
)) {
await for (final txt in client.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(serviceName),
)) {
final txtMap = <String, String>{};
final dynamic txtRaw = txt.text;
await _parseTxtRecord(txtRaw, txtMap, logger);
final device = deviceFactory(
ip: ip.address.address,
port: srv.port,
serviceName: serviceName,
txtMap: txtMap,
mdnsTypes: [mdnsType],
);
final deviceId =
'${mdnsType}_${(device.id ?? device.location ?? "${device.ip}_${device.name}")}';
if (deviceMap.containsKey(deviceId)) {
await logger.debug(
'debug.duplicate_device',
tag: logTag,
params: {'id': deviceId, 'name': device.name, 'ip': device.ip},
);
continue;
}
deviceMap[deviceId] = device;
await logger.info(
'info.found_device',
tag: logTag,
params: {
'deviceType': device.type.toString(),
'name': device.name,
'ip': device.ip,
'model': device.model,
},
);
if (onDeviceFound != null) {
onDeviceFound(device);
}
}
}
}
});
subscriptions.add(ptrSubscription);
}
try {
// 首次查詢
await sendPtrQuery();
// 週期性查詢
periodicQueryTimer = Timer.periodic(sendQueryMessageInterval, (timer) {
if (isCompleted) return;
sendPtrQuery();
});
// Wait for scanDuration to complete (DLNA style)
Future.delayed(scanDuration, () {
if (!completer.isCompleted) completer.complete();
});
try {
await completer.future;
} catch (e) {
rethrow;
}
isCompleted = true;
periodicQueryTimer.cancel();
for (final sub in subscriptions) {
await sub.cancel();
}
} catch (e) {
if (e is SocketException && e.osError?.errorCode == 1) {
await logger.error(
'errors.mdns_send_permission_denied',
tag: logTag,
params: {'error': e.toString()},
);
throw Exception(
'mDNS permission denied: unable to send multicast packets. Please ensure you have network and multicast permissions, or run as root/administrator.',
);
}
await logger.error('errors.scan_failed', tag: logTag, error: e);
} finally {
bool clientStopped = false;
if (!clientStopped) {
try {
client.stop();
} catch (_) {}
clientStopped = true;
}
periodicQueryTimer?.cancel();
for (final sub in subscriptions) {
await sub.cancel();
}
}
devices.addAll(deviceMap.values);
await logger.info(
'info.scan_complete',
tag: logTag,
params: {'count': devices.length},
);
return devices;
}