Skip to content

Commit

Permalink
update: upgraded xml dependency, added mdns finder
Browse files Browse the repository at this point in the history
  • Loading branch information
Terrabythia committed Sep 1, 2020
1 parent 270dbd0 commit 9ad70bf
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 191 deletions.
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <IntanceName> _googlecast._tcp. local.`

Copy the name (without the port) directly after the text '<IntanceName> can be reached at '...

`$ dns-sd -Gv4v6 <Paste>`

---

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`.

Expand Down
119 changes: 69 additions & 50 deletions index.dart
Original file line number Diff line number Diff line change
@@ -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<String> arguments) {

void main(List<String> 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<CastMedia> media = argResults.rest.map((String i) => CastMedia(contentId: i)).toList();
final List<CastMedia> 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<find_chromecast.CastDevice> 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<CastMedia> 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<CastMedia> 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;
Expand All @@ -57,8 +87,7 @@ void startCasting(List<CastMedia> 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());
}
Expand All @@ -81,35 +110,34 @@ void startCasting(List<CastMedia> 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;
Expand Down Expand Up @@ -146,10 +174,7 @@ void startCasting(List<CastMedia> 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
Expand All @@ -160,39 +185,33 @@ void startCasting(List<CastMedia> media, String host, int port, bool append) asy
stdin.lineMode = false;

stdin.asBroadcastStream().listen((List<int> data) {
_handleUserInput(castSender, data);
_handleUserInput(castSender, data);
});
// stdin.asBroadcastStream().listen(_handleUserInput);

}

void _handleUserInput(CastSender castSender, List<int> data) {

if (null == castSender || data.length == 0) return;

int keyCode = data.last;

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

}

}
40 changes: 18 additions & 22 deletions lib/casting/cast_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@ 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;
String _sourceId;
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();
Expand All @@ -35,25 +35,21 @@ abstract class CastChannel {
castMessage.payloadUtf8 = jsonEncode(payload);

Uint8List bytes = castMessage.writeToBuffer();
Uint32List headers = Uint32List.fromList(writeUInt32BE(List<int>(4), bytes.lengthInBytes));
Uint32List fullData = Uint32List.fromList(headers.toList()..addAll(bytes.toList()));
Uint32List headers =
Uint32List.fromList(writeUInt32BE(List<int>(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++;

}

}
}
16 changes: 5 additions & 11 deletions lib/casting/cast_device.dart
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,9 +23,7 @@ enum GoogleCastModelType {
CastGroup,
}


class CastDevice extends ChangeNotifier {

final Logger log = new Logger('CastDevice');

final String name;
Expand Down Expand Up @@ -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 {
Expand All @@ -79,8 +76,7 @@ class CastDevice extends ChangeNotifier {
if (null != deviceInfo['model_name']) {
_modelName = deviceInfo['model_name'];
}
}
catch(exception) {
} catch (exception) {
_friendlyName = 'Unknown';
}
}
Expand All @@ -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;
Expand Down Expand Up @@ -128,5 +123,4 @@ class CastDevice extends ChangeNotifier {
return GoogleCastModelType.NonGoogle;
}
}

}
Loading

0 comments on commit 9ad70bf

Please sign in to comment.