scanDlnaRendererDevices function
Future<List<DiscoveredDevice> >
scanDlnaRendererDevices({})
Scan for DLNA Renderer devices in the local network (using SSDP/UPnP)
onDeviceFound
回調函數,當找到新裝置時調用
Implementation
Future<List<DiscoveredDevice>> scanDlnaRendererDevices({
Duration sendQueryMessageInterval = const Duration(seconds: 3),
Duration scanDuration = const Duration(seconds: 15),
Function(DiscoveredDevice)? onDeviceFound,
}) async {
final logger = AppLogger();
await logger.info('info.start_dlna_renderer_scan', tag: 'SSDP');
final List<DiscoveredDevice> devices = [];
RawDatagramSocket socket;
try {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
} catch (e) {
return [];
}
// SSDP discovery message
const String ssdpRequest =
'M-SEARCH * HTTP/1.1\r\n'
'HOST: 239.255.255.250:1900\r\n'
'MAN: "ssdp:discover"\r\n'
'MX: 2\r\n'
'ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n'
'\r\n';
final data = utf8.encode(ssdpRequest);
// 定期發送 SSDP discovery message
final periodic = Timer.periodic(sendQueryMessageInterval, (_) {
socket.send(data, InternetAddress('239.255.255.250'), 1900);
});
// 啟動時立即發送一次
socket.send(data, InternetAddress('239.255.255.250'), 1900);
final responses = <String, DiscoveredDevice>{};
final completer = Completer<void>();
socket.listen(
(RawSocketEvent event) async {
if (event == RawSocketEvent.read) {
final datagram = socket.receive();
if (datagram != null) {
final resp = utf8.decode(datagram.data);
final ip = datagram.address.address;
if (resp.contains('MediaRenderer')) {
// Parse device information
final nameMatch = RegExp(r'\nSERVER: (.+)').firstMatch(resp);
final name = nameMatch?.group(1) ?? 'DLNA Renderer';
// Parse LOCATION field
final locationMatch = RegExp(
r'LOCATION:\s*(.+)\r?\n',
caseSensitive: false,
).firstMatch(resp);
final location = locationMatch?.group(1)?.trim();
// Parse model information (if available)
final modelMatch = RegExp(
r'MODEL: (.+?)\r?\n',
caseSensitive: false,
).firstMatch(resp);
final model = modelMatch?.group(1)?.trim();
String? avTransportControlUrl;
String? renderingControlUrl;
if (location != null) {
try {
final urls = await fetchControlUrls(location);
avTransportControlUrl = urls[0];
renderingControlUrl = urls[1];
} catch (e) {
await logger.error(
'errors.parse_control_urls_failed',
tag: 'SSDP',
error: e,
);
}
}
if (!responses.containsKey(ip)) {
final device = DiscoveredDevice.fromDlnaRenderer(
name: name,
ip: ip,
location: location ?? '',
avTransportControlUrl: avTransportControlUrl,
renderingControlUrl: renderingControlUrl,
model: model,
);
await logger.info(
'info.found_dlna_renderer',
tag: 'SSDP',
params: {
'name': device.name,
'ip': device.ip,
'model': device.model ?? 'unknown',
'location': device.location,
},
);
responses[ip] = device;
if (onDeviceFound != null) {
onDeviceFound(device);
}
}
}
}
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
onError: (e) {
if (!completer.isCompleted) {
completer.complete();
}
},
);
// Wait for scanDuration to complete
Future.delayed(scanDuration, () {
periodic.cancel();
socket.close();
if (!completer.isCompleted) {
completer.complete();
}
});
try {
await completer.future;
} catch (e) {
rethrow;
}
devices.addAll(responses.values);
await logger.info(
'info.dlna_renderer_scan_complete',
tag: 'SSDP',
params: {'count': devices.length},
);
return devices;
}