From 0142ac8ab6cd91de1cffcaac9515d43e663bbf11 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 22 Dec 2025 16:26:05 +0100 Subject: [PATCH 1/2] Added searchStream method to allow discoveries in a stream Added case for service lost event Improved stream cancelation after discovery stops --- lib/device.dart | 48 +++++++++++++- lib/discovery_service.dart | 127 +++++++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 40 deletions(-) diff --git a/lib/device.dart b/lib/device.dart index a97476c..d61dec4 100644 --- a/lib/device.dart +++ b/lib/device.dart @@ -1,3 +1,5 @@ +import 'package:bonsoir/bonsoir.dart'; + class CastDevice { /// unique across network final String serviceName; @@ -18,8 +20,52 @@ class CastDevice { }); @override - bool operator ==(Object other) => identical(this, other) || (other is CastDevice && runtimeType == other.runtimeType && other.serviceName == serviceName); + bool operator ==(Object other) => + identical(this, other) || + (other is CastDevice && + runtimeType == other.runtimeType && + other.serviceName == serviceName); @override int get hashCode => serviceName.hashCode; } + +class CastDeviceWAvailabilty extends CastDevice { + // Specifies if device was found or has been lost + final bool isAvailable; + + CastDeviceWAvailabilty({ + required this.isAvailable, + required super.serviceName, + required super.name, + required super.host, + required super.port, + }); + + factory CastDeviceWAvailabilty.fromBonsoirServiceEvent( + BonsoirService service, + bool isAvailable, + ) { + final port = service.port; + final host = + service.toJson()['service.ip'] ?? service.toJson()['service.host']; + if (host == null) { + throw 'Could not resolve service'; + } + + String name = [ + service.attributes?['md'], + service.attributes?['fn'], + ].whereType().join(' - '); + if (name.isEmpty) { + name = service.name; + } + return CastDeviceWAvailabilty( + serviceName: service.name, + name: name, + host: host, + port: port, + isAvailable: isAvailable, + ); + } +} diff --git a/lib/discovery_service.dart b/lib/discovery_service.dart index e85714b..c14a47b 100644 --- a/lib/discovery_service.dart +++ b/lib/discovery_service.dart @@ -14,53 +14,102 @@ class CastDiscoveryService { return _instance; } - Future> search({Duration timeout = const Duration(seconds: 5)}) async { - final results = []; + /// Searches for Cast devices and returns a List of discovered devices. + /// This method is synchronous, meaning it will wait for [timeout] duration. + /// Use [searchStream] for asynchronous (stream) reponse. + /// + /// The returned list containes resolved Devices (host and port is already resolved). + /// The discovery process runs for [timeout] and then stops. + Future> search( + {Duration timeout = const Duration(seconds: 5)}) async { + final results = {}; final discovery = BonsoirDiscovery(type: _domain); await discovery.ready; - - discovery.eventStream!.listen((event) { - if (event.type == BonsoirDiscoveryEventType.discoveryServiceFound) { - event.service?.resolve(discovery.serviceResolver); - } else if (event.type == BonsoirDiscoveryEventType.discoveryServiceResolved) { - if (event.service == null || event.service?.attributes == null) { - return; + late StreamSubscription subscription; + try { + subscription = discovery.eventStream!.listen((event) { + final availEvent = + _serviceEventHandler(event, discovery.serviceResolver); + if (availEvent != null) { + if (availEvent.isAvailable) { + results.add(availEvent); + } else { + results.remove(availEvent); + } } + }, onError: (error) { + print('[CastDiscoveryService] error ${error.runtimeType} - $error'); + }); - final port = event.service?.port; - final host = event.service?.toJson()['service.ip'] ?? event.service?.toJson()['service.host']; - - String name = [ - event.service?.attributes?['md'], - event.service?.attributes?['fn'], - ].whereType().join(' - '); - if (name.isEmpty) { - name = event.service!.name; - } + await discovery.start(); + await Future.delayed(timeout); + await discovery.stop(); + } finally { + await subscription.cancel(); + } - if (port == null || host == null) { - return; - } + return results.toList(); + } - results.add( - CastDevice( - serviceName: event.service!.name, - name: name, - port: port, - host: host, - extras: event.service!.attributes ?? {}, - ), - ); - } - }, onError: (error) { - print('[CastDiscoveryService] error ${error.runtimeType} - $error'); - }); + /// Searches for Cast devices and returns a stream of discovered devices. + /// Always check for [isAvailable] attribute to check if device is no longer active. + /// + /// The returned stream emits a `CastDevice` each time a device is Resolved. + /// + /// The discovery process runs for [timeout] and then stops and the stream + /// is closed. + Stream searchStream({ + Duration timeout = const Duration(seconds: 5), + }) async* { + final discovery = BonsoirDiscovery(type: _domain); + late Stream stream; - await discovery.start(); - await Future.delayed(timeout); - await discovery.stop(); + try { + await discovery.ready; + stream = discovery.eventStream! + .map( + (event) => _serviceEventHandler(event, discovery.serviceResolver)) + .where((d) => d != null) + .cast() + .timeout( + timeout, + onTimeout: (sink) { + sink.close(); + }, + ); + await discovery.start(); + yield* stream; + } finally { + await discovery.stop(); + } + } - return results.toSet().toList(); + // Event handler for bonsoir discovery event stream. + // Resolves the service if not yet done, and returns CastDeviceAvailabilityEvent for + // resolved service events (found and lost). Other events are dismissed. + CastDeviceWAvailabilty? _serviceEventHandler( + BonsoirDiscoveryEvent event, + ServiceResolver serviceResolver, + ) { + switch (event.type) { + case BonsoirDiscoveryEventType.discoveryServiceFound: + event.service?.resolve(serviceResolver); + break; + case BonsoirDiscoveryEventType.discoveryServiceResolved: + case BonsoirDiscoveryEventType.discoveryServiceLost: + if (event.service == null || event.service!.attributes == null) { + return null; + } else { + final resolved = + event.type == BonsoirDiscoveryEventType.discoveryServiceResolved; + return CastDeviceWAvailabilty.fromBonsoirServiceEvent( + event.service!, + resolved, + ); + } + default: + return null; + } } } From 0d49e415d233d066fb7d23ff0f764c33772b687d Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 22 Dec 2025 17:22:51 +0100 Subject: [PATCH 2/2] Added missing extras attibute to CastDeviceWAvailabilty class --- lib/device.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/device.dart b/lib/device.dart index d61dec4..c5251b2 100644 --- a/lib/device.dart +++ b/lib/device.dart @@ -40,6 +40,7 @@ class CastDeviceWAvailabilty extends CastDevice { required super.name, required super.host, required super.port, + required super.extras, }); factory CastDeviceWAvailabilty.fromBonsoirServiceEvent( @@ -65,6 +66,7 @@ class CastDeviceWAvailabilty extends CastDevice { name: name, host: host, port: port, + extras: service.attributes ?? {}, isAvailable: isAvailable, ); }