scanDlnaRendererDevices function

Future<List<DiscoveredDevice>> scanDlnaRendererDevices({
  1. Duration sendQueryMessageInterval = const Duration(seconds: 3),
  2. Duration scanDuration = const Duration(seconds: 15),
  3. dynamic onDeviceFound(
    1. DiscoveredDevice
    )?,
})

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;
}