probe method

Future<void> probe()

Sends a WS-Discovery Probe message on the ONVIF multicast group and collects ProbeMatches responses for _timeout.

Implementation

Future<void> probe() async {
  _matches.clear();

  final socket = createUdp4Socket();
  final completer = Completer<void>();

  final messageListener = ((JSUint8Array buffer, RemoteInfo rinfo) {
    try {
      final bytes = buffer.toDart;
      final xml = String.fromCharCodes(bytes);

      loggy.debug(
        'RESPONSE from ${rinfo.address.toDart}:${rinfo.port.toDartInt}:\n$xml',
      );

      final envelope = Envelope.fromXmlString(xml);
      final response = envelope.body.response;

      if (response == null) return;

      _matches.addAll(ProbeMatches.fromJson(response).probeMatches);
    } catch (e, st) {
      loggy.warning('Failed to parse discovery response: $e\n$st');
    }
  }).toJS;

  final errorListener = ((JSObject err) {
    final code = err.getProperty<JSAny?>('code'.toJS);
    final codeStr = code?.isA<JSString>() == true
        ? (code! as JSString).toDart
        : '';

    loggy.warning(
      'dgram socket error $codeStr (non-fatal, continuing probe): $err',
    );

    // EADDRINUSE is the only error from which we cannot recover at all.
    if (codeStr == 'EADDRINUSE' || codeStr == 'EACCES') {
      if (!completer.isCompleted) completer.completeError(err);
    }
  }).toJS;

  socket.on('message'.toJS, messageListener);
  socket.on('error'.toJS, errorListener);

  final listeningListener = (() {
    // Join the ONVIF multicast group so we receive `ProbeMatches`.
    try {
      socket.addMembership(_broadcastAddress.toJS);
    } catch (e) {
      loggy.warning('addMembership failed (non-fatal): $e');
    }

    _sendHello(socket);
    _sendProbe(socket);
  }).toJS;

  socket.on('listening'.toJS, listeningListener);

  // Bind to an ephemeral port; Node will emit 'listening' when ready.
  socket.bind();

  // Honor the timeout, then send Bye and close.
  final timer = Timer(_timeout, () async {
    try {
      _sendBye(socket);
    } catch (_) {
      /* best effort */
    }

    // Node's dgram `socket.close([callback])` invokes the callback with
    // zero arguments when it finishes. Under dart2js, a 1-arg closure
    // wrapped by `.toJS` only has a `call$1` signature, and the bridge
    // crashes with `call$0 is not a function` if JS calls it with 0
    // args. Use a 0-arg closure to match the real arity.
    socket.close(
      (() {
        if (!completer.isCompleted) completer.complete();
      }).toJS,
    );
  });

  try {
    await completer.future;
  } finally {
    if (timer.isActive) timer.cancel();
  }
}