Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion lib/device.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:bonsoir/bonsoir.dart';

class CastDevice {
/// unique across network
final String serviceName;
Expand All @@ -18,8 +20,54 @@ 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,
required super.extras,
});

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<String>().join(' - ');
if (name.isEmpty) {
name = service.name;
}
return CastDeviceWAvailabilty(
serviceName: service.name,
name: name,
host: host,
port: port,
extras: service.attributes ?? {},
isAvailable: isAvailable,
);
}
}
127 changes: 88 additions & 39 deletions lib/discovery_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,102 @@ class CastDiscoveryService {
return _instance;
}

Future<List<CastDevice>> search({Duration timeout = const Duration(seconds: 5)}) async {
final results = <CastDevice>[];
/// 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<List<CastDevice>> search(
{Duration timeout = const Duration(seconds: 5)}) async {
final results = <CastDevice>{};

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<BonsoirDiscoveryEvent> 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<String>().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<CastDeviceWAvailabilty> searchStream({
Duration timeout = const Duration(seconds: 5),
}) async* {
final discovery = BonsoirDiscovery(type: _domain);
late Stream<CastDeviceWAvailabilty> 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<CastDeviceWAvailabilty>()
.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;
}
}
}