johnvuko / flutter_cast

Dart package to discover and connect with Chromecast devices
MIT License
46 stars 38 forks source link

How do I fully disconnect from my chromecast? #21

Open vanlooverenkoen opened 3 years ago

vanlooverenkoen commented 3 years ago

This is how I disconnect in code:

    session.sendMessage(CastSession.kNamespaceReceiver, {
      'type': 'STOP',
      'sessionId': _appSessionId,
    });
    await CastSessionManager().endSession(session.sessionId);
    await _castSessionStateStream?.cancel();
    await _messageStream?.cancel();

I followed this document: https://docs.rs/crate/gcast/0.1.5/source/PROTOCOL.md

#### `STOP` (Client -> Cast device)

This is a textual message with one data field: `sessionId`.

This message will stop an application running with the given session identifier.

A list of running applications and their session ids can be obtained from reading
a `RECEIVER_STATUS` message from the Cast device.

```json
{
    "type": "STOP",
    "sessionId": "f2f6a2c3-2c92-4c43-9fb2-ca0b2872a75d"
}

It is always transmitted on the urn:x-cast:com.google.cast.receiver namespace.


If I try to call `session.close()` again I get the following error:

Unhandled Exception: Bad state: StreamSink is closed


Which is correct I think.

Do you have an idea how I can fully cleanup my receiver device?
josefrvaldes commented 3 years ago

Any update here? I have the same problem.

Neither CastSessionManager().endSession(sessionId), or session.close() or _session?.sendMessage(CastSession.kNamespaceConnection, {'type': 'CLOSE'}) are working. I mean, they do disconnect my app from the chromecast device, but my app is still launched in the device. I want the chromecast to totally close my app and show the "nothing connected" landscape wallpapers.

vanlooverenkoen commented 3 years ago

Not yet :(

johnvuko commented 3 years ago

It probably come from the code used in the receiver app (the app running on the Chromecast). There are events like cast.framework.system.EventType.SENDER_DISCONNECTED https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.EventType

jeffaknine commented 2 years ago

Any update on this ?

vanlooverenkoen commented 2 years ago

Not from my side

jeffaknine commented 2 years ago

So i've managed to disconnect correctly so it doesn't show the app :

Future<dynamic> close() async {
    if (!_messageController.isClosed) {
      sendMessage(kNamespaceConnection, {
        'type': 'CLOSE',
      });
      try {
        await _subscription?.cancel();
        await _socket.flush();
        _state = CastSessionState.closed;
        notifyListeners();
      } catch (_error) {
        print("error closing " + _error.toString());
      }
    }
    return _socket.close();
  }

The issue i'm having now is that i can't reconnect after a disconnect. I need to hot restart. I've changed the session_manager to a session_provider with a bit more functionality if anyone is interested.

session_provider.dart ``` import 'dart:async'; import 'dart:math'; import 'package:bonsoir/bonsoir.dart'; import 'package:flutter/material.dart'; import 'package:streaming_app/cast/cast.dart'; enum CastSessionState { connecting, connected, closed, } enum PlayerState { stopped, playing, paused, buffering, } class SessionProvider extends ChangeNotifier { static const kNamespaceConnection = 'urn:x-cast:com.google.cast.tp.connection'; static const kNamespaceHeartbeat = 'urn:x-cast:com.google.cast.tp.heartbeat'; static const kNamespaceReceiver = 'urn:x-cast:com.google.cast.receiver'; static const kNamespaceDeviceauth = 'urn:x-cast:com.google.cast.tp.deviceauth'; static const kNamespaceMedia = 'urn:x-cast:com.google.cast.media'; String? sessionId; int? _mediaSessionId; CastSocket get socket => _socket; CastSessionState get state => _state; PlayerState get playerState => _playerState; Stream get stateStream => _stateController.stream; Stream> get messageStream => _messageController.stream; CastDevice? get device => _device; List get devices => _devices; double? get volume => _volume; double? get playbackPosition => _playbackPosition; late CastSocket _socket; CastSessionState _state = CastSessionState.connecting; PlayerState _playerState = PlayerState.stopped; String? _transportId; CastDevice? _device; final _stateController = StreamController.broadcast(); final _messageController = StreamController>.broadcast(); final List _devices = []; BonsoirDiscovery discovery = BonsoirDiscovery(type: "_googlecast._tcp"); StreamSubscription? _subscription; double? _volume; double? _playbackPosition; discoverDevices() async { await discovery.ready; if (!discovery.isReady) { _devices.clear(); notifyListeners(); discovery = BonsoirDiscovery(type: "_googlecast._tcp"); await discovery.ready; } print(discovery.isReady); await discovery.start(); discovery.eventStream!.listen((event) { if (event.type == BonsoirDiscoveryEventType.DISCOVERY_SERVICE_RESOLVED) { if (event.service == null || event.service?.attributes == null) { return; } final port = event.service!.port; final host = event.service?.toJson()['service.ip']; String name = [ event.service?.attributes?['md'], event.service?.attributes?['fn'] ].whereType().join(' - '); if (name.isEmpty) { name = event.service!.name; } if (host == null) { return; } _devices.add(CastDevice( serviceName: event.service!.name, name: name, port: port, host: host, extras: event.service!.attributes ?? {}, )); notifyListeners(); } }); } Future connect(String sessionId, CastDevice device, [Duration? timeout]) async { _socket = await CastSocket.connect( device.host, device.port, timeout, ); notifyListeners(); discovery.stop(); _startListening(); sendMessage(kNamespaceConnection, { 'type': 'CONNECT', }); } Future close() async { if (!_messageController.isClosed) { sendMessage(kNamespaceConnection, { 'type': 'CLOSE', }); try { await _subscription?.cancel(); await _socket.flush(); _state = CastSessionState.closed; notifyListeners(); print('socket flushed'); } catch (_error) { print("error closing " + _error.toString()); } } print("HEERREEE"); return _socket.close(); } void _startListening() { _subscription = _socket.stream.listen( (message) { // happen if (_messageController.isClosed) { return; } if (message.namespace == kNamespaceHeartbeat && message.payload['type'] == 'PING') { sendMessage(kNamespaceHeartbeat, { 'type': 'PONG', }); } else if (message.namespace == kNamespaceConnection && message.payload['type'] == 'CLOSE') { close(); } else if (message.namespace == kNamespaceReceiver && message.payload['type'] == 'RECEIVER_STATUS') { _handleReceiverStatus(message.payload); _messageController.add(message.payload); } else { _messageController.add(message.payload); } }, onError: (error) { _messageController.addError(error); }, onDone: () { _messageController.close(); _state = CastSessionState.closed; _stateController.add(_state); _stateController.close(); notifyListeners(); }, cancelOnError: false, ); } void load(message) { sendMessage(SessionProvider.kNamespaceMedia, { 'type': 'LOAD', 'autoPlay': true, 'currentTime': 0, 'media': message, }); } void stop() { sendMessage(kNamespaceMedia, { 'type': 'STOP', 'mediaSessionId': _mediaSessionId, }); } void pause() { sendMessage(kNamespaceMedia, { 'type': 'PAUSE', 'mediaSessionId': _mediaSessionId, }); } void play() { sendMessage(kNamespaceMedia, { 'type': 'PLAY', 'mediaSessionId': _mediaSessionId, }); } void setVolume(double value) { sendMessage(kNamespaceMedia, { 'type': 'SET_VOLUME', 'volume': {'level': value}, 'mediaSessionId': _mediaSessionId, }); } void setMute(bool value) { sendMessage(kNamespaceMedia, { 'type': 'SET_VOLUME', 'volume': {'muted': value}, 'mediaSessionId': _mediaSessionId, }); } void seek(double value) { sendMessage(kNamespaceMedia, { 'type': 'SEEK', 'mediaSessionId': _mediaSessionId, 'currentTime': value, }); } void _handleReceiverStatus(Map payload) { if (_transportId != null) { return; } if (payload['status']?.containsKey('applications') == true) { _transportId = payload['status']['applications'][0]['transportId']; // reconnect with new _transportId sendMessage(kNamespaceConnection, { 'type': 'CONNECT', }); _state = CastSessionState.connected; _stateController.add(_state); } } void sendMessage(String namespace, Map payload) { _socket.sendMessage( namespace, sessionId!, _transportId ?? 'receiver-0', payload, ); } Future flush() { return _socket.flush(); } Future startSession(CastDevice device, [Duration? timeout]) async { sessionId = 'client-${Random().nextInt(99999)}'; await connect(sessionId!, device, timeout); _device = device; messageStream.listen((message) { if (message['type'] == 'MEDIA_STATUS' && message['status'].isNotEmpty) { switch (message['status'][0]['playerState'] as String) { case 'PLAYING': _playerState = PlayerState.playing; break; case 'PAUSED': _playerState = PlayerState.paused; break; case 'BUFFERING': _playerState = PlayerState.buffering; break; case 'IDLE': _playerState = PlayerState.stopped; break; } _mediaSessionId = message['status'][0]['mediaSessionId']; _volume = double.parse((message['status'][0]['volume']['level']).toString()); _playbackPosition = double.parse((message['status'][0]['currentTime']).toString()); } notifyListeners(); }); sendMessage(SessionProvider.kNamespaceReceiver, { 'type': 'LAUNCH', 'appId': 'CC1AD845', // set the appId of your app here }); notifyListeners(); } } ```
jeffaknine commented 2 years ago

@jonathantribouharet could you provide any insight on how to disconnect and reconnect without having to force close and reopen the app ? When I try to reconnect I get a {type: CLOSE} in the socket.listen

johnvuko commented 2 years ago

Sorry me neither, I don't know the protocol. When you try to connect again, do you use the same session or do you create a new one? I think you should not keep the same session.

jeffaknine commented 2 years ago

I am creating a new one : sessionId = 'client-${Random().nextInt(99999)}';