diff --git a/README.md b/README.md index 016d733..7c2e458 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,37 @@ Dart package to play videos to a chromecast device Simplified port of https://github.com/thibauts/node-castv2-client. -Originally designed to work in Flutter with the flutter_mdns_plugin https://github.com/terrabythia/flutter_mdns_plugin, -so this cli project does not include a mdns browser, you should find out what the local ip address and port of your ChromeCast is yourself. +Update 0.2.0: added MDNS finder, you can now omit the --host parameter and it will ask you which chromecast to use + +--- + +### Find the IP address of your chromecast on mac OS + +This is the way I found the IP address of my ChromeCast on my Mac. This is not guaranteed to work for everyone, +but if it helps anyone, here are the terminal commands: + +`$ dns-sd -B _googlecast local` + +Copy the instance name + +`$ dns-sd -L _googlecast._tcp. local.` + +Copy the name (without the port) directly after the text ' can be reached at '... + +`$ dns-sd -Gv4v6 ` + +--- See https://github.com/terrabythia/flutter_chromecast_example for an example implementation in Flutter of both the flutter_mdns_plugin and this repository. +--- + +## usage + ### options **media** space separated list of one or more media source urls -**host** IP address of a ChromeCast device in the same network that you are on. +**host** (optional) IP address of a ChromeCast device in the same network that you are on. **port** (optional) port of the ChromeCast device. Defaults to `8009`. diff --git a/index.dart b/index.dart index 8711cfd..3fc809c 100644 --- a/index.dart +++ b/index.dart @@ -1,54 +1,84 @@ -import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:logging/logging.dart'; import 'package:args/args.dart'; import 'package:dart_chromecast/casting/cast.dart'; +import 'package:dart_chromecast/utils/mdns_find_chromecast.dart' + as find_chromecast; +import 'package:logging/logging.dart'; final Logger log = new Logger('Chromecast CLI'); -void main(List arguments) { - +void main(List arguments) async { // Create an argument parser so we can read the cli's arguments and options final parser = new ArgParser() - ..addOption('host', abbr: 'h', defaultsTo: '192.168.1.214') + ..addOption('host', abbr: 'h', defaultsTo: '') ..addOption('port', abbr: 'p', defaultsTo: '8009') ..addFlag('append', abbr: 'a', defaultsTo: false) ..addFlag('debug', abbr: 'd', defaultsTo: false); final ArgResults argResults = parser.parse(arguments); - if (true == argResults['debug'] ) { + if (true == argResults['debug']) { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((LogRecord rec) { print('${rec.level.name}: ${rec.message}'); }); - } - else { + } else { Logger.root.level = Level.OFF; } // turn each rest argument string into a CastMedia instance - final List media = argResults.rest.map((String i) => CastMedia(contentId: i)).toList(); + final List media = + argResults.rest.map((String i) => CastMedia(contentId: i)).toList(); + + String host = argResults['host']; + int port = int.parse(argResults['port']); + if ('' == host.trim()) { + // search! + print('Looking for ChromeCast devices...'); + + List devices = + await find_chromecast.find_chromecasts(); + if (devices.length == 0) { + print('No devices found!'); + return; + } - startCasting(media, argResults['host'], int.parse(argResults['port']), argResults['append']); + print("Found ${devices.length} devices:"); + for (int i = 0; i < devices.length; i++) { + int index = i + 1; + find_chromecast.CastDevice device = devices[i]; + print("${index}: ${device.name}"); + } -} + print("Pick a device (1-${devices.length}):"); -void startCasting(List media, String host, int port, bool append) async { + int choice = null; - log.fine('Start Casting'); - - Function logCallback = (Object error, String logMessage) { - if (null != error) { - log.info(logMessage); - } - else { - log.warning(logMessage, error); + while (choice == null || choice < 0 || choice > devices.length) { + choice = int.parse(stdin.readLineSync()); + if (choice == null) { + print( + "Please pick a number (1-${devices.length}) or press return to search again"); + } } - }; + + find_chromecast.CastDevice pickedDevice = devices[choice - 1]; + + host = pickedDevice.ip; + port = pickedDevice.port; + + log.fine("Picked: ${pickedDevice}"); + } + + startCasting(media, host, port, argResults['append']); +} + +void startCasting( + List media, String host, int port, bool append) async { + log.fine('Start Casting'); // try to load previous state saved as json in saved_cast_state.json Map savedState; @@ -57,8 +87,7 @@ void startCasting(List media, String host, int port, bool append) asy if (null != savedStateFile) { savedState = jsonDecode(await savedStateFile.readAsString()); } - } - catch(e) { + } catch (e) { // does not exist yet log.warning('error fetching saved state' + e.toString()); } @@ -81,35 +110,34 @@ void startCasting(List media, String host, int port, bool append) asy // listen for cast session updates and save the state when // the device is connected - castSender.castSessionController.stream.listen((CastSession castSession) async { + castSender.castSessionController.stream + .listen((CastSession castSession) async { if (castSession.isConnected) { File savedStateFile = await File('./saved_cast_state.json'); Map map = { 'time': DateTime.now().millisecondsSinceEpoch, - }..addAll( - castSession.toMap() - ); - await savedStateFile.writeAsString( - jsonEncode(map) - ); + }..addAll(castSession.toMap()); + await savedStateFile.writeAsString(jsonEncode(map)); log.fine('Cast session was saved to saved_cat_state.json.'); } }); CastMediaStatus prevMediaStatus; // Listen for media status updates, such as pausing, playing, seeking, playback etc. - castSender.castMediaStatusController.stream.listen((CastMediaStatus mediaStatus) { + castSender.castMediaStatusController.stream + .listen((CastMediaStatus mediaStatus) { // show progress for example - if (null != prevMediaStatus && mediaStatus.volume != prevMediaStatus.volume) { + if (null != prevMediaStatus && + mediaStatus.volume != prevMediaStatus.volume) { // volume just updated log.info('Volume just updated to ${mediaStatus.volume}'); } - if (null == prevMediaStatus || mediaStatus?.position != prevMediaStatus?.position) { + if (null == prevMediaStatus || + mediaStatus?.position != prevMediaStatus?.position) { // update the current progress log.info('Media Position is ${mediaStatus?.position}'); } prevMediaStatus = mediaStatus; - }); bool connected = false; @@ -146,10 +174,7 @@ void startCasting(List media, String host, int port, bool append) asy } // load CastMedia playlist and send it to the chromecast - castSender.loadPlaylist( - media, - append: append - ); + castSender.loadPlaylist(media, append: append); // Initiate key press handler // space = toggle pause @@ -160,14 +185,12 @@ void startCasting(List media, String host, int port, bool append) asy stdin.lineMode = false; stdin.asBroadcastStream().listen((List data) { - _handleUserInput(castSender, data); + _handleUserInput(castSender, data); }); // stdin.asBroadcastStream().listen(_handleUserInput); - } void _handleUserInput(CastSender castSender, List data) { - if (null == castSender || data.length == 0) return; int keyCode = data.last; @@ -175,24 +198,20 @@ void _handleUserInput(CastSender castSender, List data) { if (32 == keyCode) { // space = toggle pause castSender.togglePause(); - } - else if (115 == keyCode) { + } else if (115 == keyCode) { // s == stop castSender.stop(); - } - else if (27 == keyCode) { + } else if (27 == keyCode) { // escape = disconnect castSender.disconnect(); - } - else if (67 == keyCode || 68 == keyCode) { + } else if (67 == keyCode || 68 == keyCode) { // left or right = seek 10s back or forth double seekBy = 67 == keyCode ? 10.0 : -10.0; - if (null != castSender.castSession && null != castSender.castSession.castMediaStatus) { + if (null != castSender.castSession && + null != castSender.castSession.castMediaStatus) { castSender.seek( max(0.0, castSender.castSession.castMediaStatus.position + seekBy), ); } - } - } diff --git a/lib/casting/cast_channel.dart b/lib/casting/cast_channel.dart index 485ac79..ff727ee 100644 --- a/lib/casting/cast_channel.dart +++ b/lib/casting/cast_channel.dart @@ -2,11 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import '../proto/cast_channel.pb.dart'; import './../writer.dart'; +import '../proto/cast_channel.pb.dart'; abstract class CastChannel { - static int _requestId = 1; final Socket _socket; @@ -14,16 +13,17 @@ abstract class CastChannel { String _destinationId; String _namespace; - CastChannel(this._socket, this._sourceId, this._destinationId, this._namespace); + CastChannel( + this._socket, this._sourceId, this._destinationId, this._namespace); - CastChannel.CreateWithSocket(Socket socket, { String sourceId, String destinationId, String namespace}) : - _socket = socket, - _sourceId = sourceId, - _destinationId = destinationId, - _namespace = namespace; - - void sendMessage(Map payload) async { + CastChannel.CreateWithSocket(Socket socket, + {String sourceId, String destinationId, String namespace}) + : _socket = socket, + _sourceId = sourceId, + _destinationId = destinationId, + _namespace = namespace; + void sendMessage(Map payload) async { payload['requestId'] = _requestId; CastMessage castMessage = CastMessage(); @@ -35,25 +35,21 @@ abstract class CastChannel { castMessage.payloadUtf8 = jsonEncode(payload); Uint8List bytes = castMessage.writeToBuffer(); - Uint32List headers = Uint32List.fromList(writeUInt32BE(List(4), bytes.lengthInBytes)); - Uint32List fullData = Uint32List.fromList(headers.toList()..addAll(bytes.toList())); + Uint32List headers = + Uint32List.fromList(writeUInt32BE(List(4), bytes.lengthInBytes)); + Uint32List fullData = + Uint32List.fromList(headers.toList()..addAll(bytes.toList())); if ('PING' != payload['type']) { + // print('Send: ${castMessage.toDebugString()}'); + // print('List: ${fullData.toList().toString()}'); - print('Send: ${castMessage.toDebugString()}'); - print('List: ${fullData.toList().toString()}'); - - } - else { - + } else { print('PING'); - } _socket.add(fullData); _requestId++; - } - -} \ No newline at end of file +} diff --git a/lib/casting/cast_device.dart b/lib/casting/cast_device.dart index 707dfe6..754079d 100644 --- a/lib/casting/cast_device.dart +++ b/lib/casting/cast_device.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; import 'dart:convert' show utf8; import 'dart:typed_data'; -import 'package:observable/observable.dart'; +import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; +import 'package:observable/observable.dart'; enum CastDeviceType { Unknown, @@ -23,9 +23,7 @@ enum GoogleCastModelType { CastGroup, } - class CastDevice extends ChangeNotifier { - final Logger log = new Logger('CastDevice'); final String name; @@ -67,8 +65,7 @@ class CastDevice extends ChangeNotifier { if (null != attr['md']) { _modelName = utf8.decode(attr['md']); } - } - else { + } else { // Attributes are not guaranteed to be set, if not set fetch them via the eureka_info url // Possible parameters: version,audio,name,build_info,detail,device_info,net,wifi,setup,settings,opt_in,opencast,multizone,proxy,night_mode_params,user_eq,room_equalizer try { @@ -79,8 +76,7 @@ class CastDevice extends ChangeNotifier { if (null != deviceInfo['model_name']) { _modelName = deviceInfo['model_name']; } - } - catch(exception) { + } catch (exception) { _friendlyName = 'Unknown'; } } @@ -91,8 +87,7 @@ class CastDevice extends ChangeNotifier { CastDeviceType get deviceType { if (type.contains('_googlecast._tcp')) { return CastDeviceType.ChromeCast; - } - else if (type.contains('_airplay._tcp')) { + } else if (type.contains('_airplay._tcp')) { return CastDeviceType.AppleTV; } return CastDeviceType.Unknown; @@ -128,5 +123,4 @@ class CastDevice extends ChangeNotifier { return GoogleCastModelType.NonGoogle; } } - } diff --git a/lib/casting/cast_sender.dart b/lib/casting/cast_sender.dart index 7708f82..b67db37 100644 --- a/lib/casting/cast_sender.dart +++ b/lib/casting/cast_sender.dart @@ -6,8 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:logging/logging.dart'; -import 'package:dart_chromecast/proto/cast_channel.pb.dart'; + import 'package:dart_chromecast/casting/cast_device.dart'; import 'package:dart_chromecast/casting/cast_media.dart'; import 'package:dart_chromecast/casting/cast_media_status.dart'; @@ -16,10 +15,10 @@ import 'package:dart_chromecast/casting/connection_channel.dart'; import 'package:dart_chromecast/casting/heartbeat_channel.dart'; import 'package:dart_chromecast/casting/media_channel.dart'; import 'package:dart_chromecast/casting/receiver_channel.dart'; -import 'package:dart_chromecast/casting/interfaces/log_interface.dart'; +import 'package:dart_chromecast/proto/cast_channel.pb.dart'; +import 'package:logging/logging.dart'; class CastSender extends Object { - final Logger log = new Logger('CastSender'); final CastDevice device; @@ -42,23 +41,20 @@ class CastSender extends Object { CastMedia _currentCastMedia; CastSender(this.device) { - // TODO: _airplay._tcp + // TODO: _airplay._tcp _contentQueue = []; + castSessionController = StreamController.broadcast(); castMediaStatusController = StreamController.broadcast(); } - // TODO: reconnect if there is already a current session? - // > ask the chromecast for the current session.. Future connect() async { - connectionDidClose = false; if (null == _castSession) { _castSession = CastSession( sourceId: 'client-${Random().nextInt(99999)}', - destinationId: 'receiver-0' - ); + destinationId: 'receiver-0'); } // connect to socket @@ -67,9 +63,7 @@ class CastSender extends Object { return false; } - _connectionChannel.sendMessage({ - 'type': 'CONNECT' - }); + _connectionChannel.sendMessage({'type': 'CONNECT'}); // start heartbeat _heartbeatTick(); @@ -79,32 +73,41 @@ class CastSender extends Object { // _receiverStatusTick(); return true; - } - Future reconnect({ String sourceId, String destinationId}) async { - - _castSession = CastSession(sourceId: sourceId, destinationId: destinationId); + Future reconnect({String sourceId, String destinationId}) async { + _castSession = + CastSession(sourceId: sourceId, destinationId: destinationId); bool connected = await connect(); if (!connected) { return false; } - _mediaChannel = MediaChannel.Create(socket: _socket, sourceId: sourceId, destinationId: destinationId); - _mediaChannel.sendMessage({ - 'type': 'GET_STATUS' - }); + _mediaChannel = MediaChannel.Create( + socket: _socket, sourceId: sourceId, destinationId: destinationId); + _mediaChannel.sendMessage({'type': 'GET_STATUS'}); // now wait for the media to actually get a status? bool didReconnect = await _waitForMediaStatus(); if (didReconnect) { log.fine('reconnecting successful!'); - castSessionController.add( - _castSession - ); - castMediaStatusController.add( - _castSession.castMediaStatus - ); + try { + castSessionController.add(_castSession); + } catch (e) { + log.severe( + "Could not add the CastSession to the CastSession Stream Controller: events will not be triggered"); + log.severe(e.toString()); + log.info("Closed? ${castSessionController.isClosed}"); + } + + try { + castMediaStatusController.add(_castSession.castMediaStatus); + } catch (e) { + log.severe( + "Could not add the CastMediaStatus to the CastSession Stream Controller: events will not be triggered"); + log.severe(e.toString()); + log.info("Closed? ${castMediaStatusController.isClosed}"); + } } return didReconnect; } @@ -137,11 +140,11 @@ class CastSender extends Object { loadPlaylist([media], forceNext: forceNext); } - void loadPlaylist(List media, { append = false, forceNext = false }) { + void loadPlaylist(List media, + {append = false, forceNext = false}) { if (!append) { _contentQueue = media; - } - else { + } else { _contentQueue.addAll(media); } if (null != _mediaChannel) { @@ -152,10 +155,11 @@ class CastSender extends Object { void _castMediaAction(type, [params]) { if (null == params) params = {}; if (null != _mediaChannel && null != _castSession?.castMediaStatus) { - _mediaChannel.sendMessage(params..addAll({ - 'mediaSessionId': _castSession.castMediaStatus.sessionId, - 'type': type, - })); + _mediaChannel.sendMessage(params + ..addAll({ + 'mediaSessionId': _castSession.castMediaStatus.sessionId, + 'type': type, + })); } } @@ -174,8 +178,7 @@ class CastSender extends Object { log.info(_castSession?.castMediaStatus.toString()); if (true == _castSession?.castMediaStatus?.isPlaying) { pause(); - } - else if (true == _castSession?.castMediaStatus?.isPaused){ + } else if (true == _castSession?.castMediaStatus?.isPaused) { play(); } } @@ -185,12 +188,12 @@ class CastSender extends Object { } void seek(double time) { - Map map = { 'currentTime': time }; + Map map = {'currentTime': time}; _castMediaAction('SEEK', map); } void setVolume(double volume) { - Map map = { 'volume': min(volume, 1)}; + Map map = {'volume': min(volume, 1)}; _castMediaAction('VOLUME', map); } @@ -201,26 +204,24 @@ class CastSender extends Object { Future _createSocket() async { if (null == _socket) { try { - log.fine('Connecting to ${device.host}:${device.port}'); - _socket = await SecureSocket.connect( - device.host, - device.port, + _socket = await SecureSocket.connect(device.host, device.port, onBadCertificate: (X509Certificate certificate) => true, timeout: Duration(seconds: 10)); - _connectionChannel = ConnectionChannel.create(_socket, sourceId: _castSession.sourceId, destinationId: _castSession.destinationId); - _heartbeatChannel = HeartbeatChannel.create(_socket, sourceId: _castSession.sourceId, destinationId: _castSession.destinationId); - _receiverChannel = ReceiverChannel.create(_socket, sourceId: _castSession.sourceId, destinationId: _castSession.destinationId); - - _socket.listen( - _onSocketData, - onDone: _dispose - ); - - } - catch(e) { + _connectionChannel = ConnectionChannel.create(_socket, + sourceId: _castSession.sourceId, + destinationId: _castSession.destinationId); + _heartbeatChannel = HeartbeatChannel.create(_socket, + sourceId: _castSession.sourceId, + destinationId: _castSession.destinationId); + _receiverChannel = ReceiverChannel.create(_socket, + sourceId: _castSession.sourceId, + destinationId: _castSession.destinationId); + + _socket.listen(_onSocketData, onDone: _dispose); + } catch (e) { log.fine(e.toString()); return null; } @@ -243,8 +244,7 @@ class CastSender extends Object { } if ('RECEIVER_STATUS' == payloadMap['type']) { _handleReceiverStatus(payloadMap); - } - else if ('MEDIA_STATUS' == payloadMap['type']) { + } else if ('MEDIA_STATUS' == payloadMap['type']) { _handleMediaStatus(payloadMap); } } @@ -253,27 +253,35 @@ class CastSender extends Object { void _handleReceiverStatus(Map payload) { log.fine(payload.toString()); - if (null == _mediaChannel && true == payload['status']?.containsKey('applications')) { + if (null == _mediaChannel && + true == payload['status']?.containsKey('applications')) { // re-create the channel with the transportId the chromecast just sent us if (false == _castSession?.isConnected) { - _castSession = _castSession..mergeWithChromeCastSessionMap(payload['status']['applications'][0]); - _connectionChannel = ConnectionChannel.create(_socket, sourceId: _castSession.sourceId, destinationId: _castSession.destinationId); - _connectionChannel.sendMessage({ - 'type': 'CONNECT' - }); - _mediaChannel = MediaChannel.Create(socket: _socket, sourceId: _castSession.sourceId, destinationId: _castSession.destinationId); - _mediaChannel.sendMessage({ - 'type': 'GET_STATUS' - }); - castSessionController.add( - _castSession - ); + _castSession = _castSession + ..mergeWithChromeCastSessionMap(payload['status']['applications'][0]); + _connectionChannel = ConnectionChannel.create(_socket, + sourceId: _castSession.sourceId, + destinationId: _castSession.destinationId); + _connectionChannel.sendMessage({'type': 'CONNECT'}); + _mediaChannel = MediaChannel.Create( + socket: _socket, + sourceId: _castSession.sourceId, + destinationId: _castSession.destinationId); + _mediaChannel.sendMessage({'type': 'GET_STATUS'}); + + try { + castSessionController.add(_castSession); + } catch (e) { + log.severe( + "Could not add the CastSession to the CastSession Stream Controller: events will not be triggered"); + log.severe(e.toString()); + } } } } Future _waitForMediaStatus() async { - while(false == _castSession.isConnected) { + while (false == _castSession.isConnected) { await Future.delayed(Duration(milliseconds: 100)); if (connectionDidClose) return false; } @@ -281,21 +289,17 @@ class CastSender extends Object { } void _handleMediaStatus(Map payload) { - - log.fine('Handle media status: ' + payload.toString()); + log.fine('Handle media status: ' + payload.toString()); if (null != payload['status']) { - if (!_castSession.isConnected) { _castSession.isConnected = true; _handleContentQueue(); } if (payload['status'].length > 0) { - - _castSession.castMediaStatus = CastMediaStatus.fromChromeCastMediaStatus( - payload['status'][0] - ); + _castSession.castMediaStatus = + CastMediaStatus.fromChromeCastMediaStatus(payload['status'][0]); log.fine('Media status ${_castSession.castMediaStatus.toString()}'); @@ -304,29 +308,32 @@ class CastSender extends Object { } if (_castSession.castMediaStatus.isPlaying) { - _mediaCurrentTimeTimer = Timer(Duration(seconds: 1), _getMediaCurrentTime); - } - else if (_castSession.castMediaStatus.isPaused && null != _mediaCurrentTimeTimer) { + _mediaCurrentTimeTimer = + Timer(Duration(seconds: 1), _getMediaCurrentTime); + } else if (_castSession.castMediaStatus.isPaused && + null != _mediaCurrentTimeTimer) { _mediaCurrentTimeTimer.cancel(); _mediaCurrentTimeTimer = null; } - castMediaStatusController.add( - _castSession.castMediaStatus - ); - - } - else { - + try { + castMediaStatusController.add(_castSession.castMediaStatus); + } catch (e) { + log.severe( + "Could not add the CastMediaStatus to the CastSession Stream Controller: events will not be triggered"); + log.severe(e.toString()); + log.info("Closed? ${castMediaStatusController.isClosed}"); + } + } else { log.fine("Media status is empty"); if (null == _currentCastMedia && _contentQueue.isNotEmpty) { - log.fine("no media is currently being casted, try to cast first in queue"); + log.fine( + "no media is currently being casted, try to cast first in queue"); _handleContentQueue(); } } } - } _handleContentQueue({forceNext = false}) { @@ -343,39 +350,31 @@ class CastSender extends Object { _currentCastMedia = _contentQueue.elementAt(0); if (null != _currentCastMedia) { _contentQueue = _contentQueue.getRange(1, _contentQueue.length).toList(); - _mediaChannel.sendMessage( - _currentCastMedia.toChromeCastMap() - ); + _mediaChannel.sendMessage(_currentCastMedia.toChromeCastMap()); } } void _getMediaCurrentTime() { - - if (null != _mediaChannel && true == _castSession?.castMediaStatus?.isPlaying) { + if (null != _mediaChannel && + true == _castSession?.castMediaStatus?.isPlaying) { _mediaChannel.sendMessage({ 'type': 'GET_STATUS', }); } - } void _heartbeatTick() { - if (null != _heartbeatChannel) { - _heartbeatChannel.sendMessage({ - 'type': 'PING' - }); + _heartbeatChannel.sendMessage({'type': 'PING'}); // _heartbeatTimer = Timer(Duration(seconds: 5), _heartbeatTick); Timer(Duration(seconds: 5), _heartbeatTick); - } - } void _dispose() { - castSessionController.close(); - castMediaStatusController.close(); + castSessionController.close(); + castMediaStatusController.close(); _socket = null; _heartbeatChannel = null; _connectionChannel = null; @@ -396,5 +395,4 @@ class CastSender extends Object { // TODO: implement logInfo return null; } - } diff --git a/lib/utils/mdns_find_chromecast.dart b/lib/utils/mdns_find_chromecast.dart new file mode 100644 index 0000000..b59b875 --- /dev/null +++ b/lib/utils/mdns_find_chromecast.dart @@ -0,0 +1,41 @@ +import 'package:multicast_dns/multicast_dns.dart'; + +class CastDevice { + final String name; + final String ip; + final int port; + + CastDevice({this.name, this.ip, this.port}); + + String toString() => "CastDevice: ${name} -> ${ip}:${port}"; +} + +Future> find_chromecasts() async { + const String name = '_googlecast._tcp.local'; + final MDnsClient client = MDnsClient(); + + final Map map = {}; + // Start the client with default options. + await client.start(); + + // Get the PTR recod for the service. + await for (PtrResourceRecord ptr in client + .lookup(ResourceRecordQuery.serverPointer(name))) { + // Use the domainName from the PTR record to get the SRV record, + // which will have the port and local hostname. + // Note that duplicate messages may come through, especially if any + // other mDNS queries are running elsewhere on the machine. + await for (SrvResourceRecord srv in client.lookup( + ResourceRecordQuery.service(ptr.domainName))) { + await for (IPAddressResourceRecord ip + in client.lookup( + ResourceRecordQuery.addressIPv4(srv.target))) { + map[srv.name] = + CastDevice(name: srv.name, ip: ip.address.address, port: srv.port); + } + } + } + client.stop(); + + return map.values.toList(); +} diff --git a/pubspec.yaml b/pubspec.yaml index 85f91a4..600440c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dart_chromecast -version: 0.1.2 +version: 0.2.0 description: Cast videos to your ChromeCast device author: Sander Bruggeman homepage: https://github.com/terrabythia/dart_chromecast @@ -13,6 +13,7 @@ dependencies: http: ^0.12.0+2 cli_util: ^0.1.3+2 args: ^1.4.0 - xml: ^3.0.0 + xml: ^4.1.0 observable: ^0.22.2 logging: ^0.11.3+2 + multicast_dns: ^0.2.2