pax_global_header00006660000000000000000000000064130635500220014506gustar00rootroot0000000000000052 comment=a867ac9a2cbbdda7f1cd747259009b8dc073e3a2 pychromecast-0.8.1/000077500000000000000000000000001306355002200142155ustar00rootroot00000000000000pychromecast-0.8.1/.gitignore000066400000000000000000000007741306355002200162150ustar00rootroot00000000000000# Hide sublime text stuff *.sublime-project *.sublime-workspace # Hide some OS X stuff .DS_Store .AppleDouble .LSOverride Icon # Thumbnails ._* # GITHUB Proposed Python stuff: *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Build files README.rst # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevprojectpychromecast-0.8.1/.travis.yml000066400000000000000000000004111306355002200163220ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.4" - "3.5" install: - pip install -r requirements.txt - pip install flake8 pylint script: - flake8 --exclude cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py pychromecast - pylint pychromecast pychromecast-0.8.1/LICENSE000066400000000000000000000020731306355002200152240ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Paulus Schoutsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pychromecast-0.8.1/MANIFEST.in000066400000000000000000000001541306355002200157530ustar00rootroot00000000000000include README.rst include LICENSE include requirements.txt graft pychromecast recursive-exclude * *.py[co] pychromecast-0.8.1/README.rst000066400000000000000000000161571306355002200157160ustar00rootroot00000000000000pychromecast |Build Status| =========================== .. |Build Status| image:: https://travis-ci.org/balloob/pychromecast.svg?branch=master :target: https://travis-ci.org/balloob/pychromecast Library for Python 2 and 3 to communicate with the Google Chromecast. It currently supports: - Auto discovering connected Chromecasts on the network - Start the default media receiver and play any online media - Control playback of current playing media - Implement Google Chromecast api v2 - Communicate with apps via channels - Easily extendable to add support for unsupported namespaces - Multi-room setups with Audio cast devices *Check out [Home Assistant](https://home-assistant.io) for a ready-made solution using PyChromecast for controlling and automating your Chromecast or Cast-enabled device like Google Home.* Dependencies ------------ PyChromecast depends on the Python packages requests, protobuf and zeroconf. Make sure you have these dependencies installed using ``pip install -r requirements.txt`` Some users running Python 2.7 have `reported`_ that they had to upgrade their version of pip using ``pip install --upgrade pip`` before they were able to install the latest version of the dependencies. .. _reported: https://github.com/balloob/pychromecast/issues/47#issuecomment-107822162 How to use ---------- .. code:: python >> from __future__ import print_function >> import time >> import pychromecast >> chromecasts = pychromecast.get_chromecasts() >> [cc.device.friendly_name for cc in chromecasts] ['Dev', 'Living Room', 'Den', 'Bedroom'] >> cast = next(cc for cc in chromecasts if cc.device.friendly_name == "Living Room") >> # Wait for cast device to be ready >> cast.wait() >> print(cast.device) DeviceStatus(friendly_name='Living Room', model_name='Chromecast', manufacturer='Google Inc.', api_version=(1, 0), uuid=UUID('df6944da-f016-4cb8-97d0-3da2ccaa380b'), cast_type='cast') >> print(cast.status) CastStatus(is_active_input=True, is_stand_by=False, volume_level=1.0, volume_muted=False, app_id=u'CC1AD845', display_name=u'Default Media Receiver', namespaces=[u'urn:x-cast:com.google.cast.player.message', u'urn:x-cast:com.google.cast.media'], session_id=u'CCA39713-9A4F-34A6-A8BF-5D97BE7ECA5C', transport_id=u'web-9', status_text='') >> mc = cast.media_controller >> mc.play_media('http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 'video/mp4') >> mc.block_until_active() >> print(mc.status) MediaStatus(current_time=42.458322, content_id=u'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', content_type=u'video/mp4', duration=596.474195, stream_type=u'BUFFERED', idle_reason=None, media_session_id=1, playback_rate=1, player_state=u'PLAYING', supported_media_commands=15, volume_level=1, volume_muted=False) >> mc.pause() >> time.sleep(5) >> mc.play() Adding support for extra namespaces ----------------------------------- Each app that runs on the Chromecast supports namespaces. They specify a JSON-based mini-protocol. This is used to communicate between the Chromecast and your phone/browser and now Python. Support for extra namespaces is added by using controllers. To add your own namespace to a current chromecast instance you will first have to define your controller. Example of a minimal controller: .. code:: python from pychromecast.controllers import BaseController class MyController(BaseController): def __init__(self): super(MyController, self).__init__( "urn:x-cast:my.super.awesome.namespace") def receive_message(self, message, data): print("Wow, I received this message: {}".format(data)) return True # indicate you handled this message def request_beer(self): self.send_message({'request': 'beer'}) After you have defined your controller you will have to add an instance to a Chromecast object: `cast.register_handler(MyController())`. When a message is received with your namespace it will be routed to your controller. For more options see the `BaseController`_. For an example of a fully implemented controller see the `MediaController`_. .. _BaseController: https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/__init__.py .. _MediaController: https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/media.py Exploring existing namespaces ------------------------------- So you've got PyChromecast running and decided it is time to add support to your favorite app. No worries, the following instructions will have you covered in exploring the possibilities. The following instructions require the use of the `Google Chrome browser`_ and the `Google Cast plugin`_. * In Chrome, go to `chrome://net-internals/#capture` * Enable the checkbox 'Include the actual bytes sent/received.' * Open a new tab, browse to your favorite application on the web that has Chromecast support and start casting. * Go back to the tab that is capturing events and click on stop. * From the dropdown click on events. This will show you a table with events that happened while you were recording. * In the filter box enter the text `Tr@n$p0rt`. This should give one SOCKET connection as result: the connection with your Chromecast. * Go through the results and collect the JSON that is exchanged. * Now write a controller that is able to mimic this behavior :-) .. _Google Chrome Browser: https://www.google.com/chrome/ .. _Google Cast Plugin: https://chrome.google.com/webstore/detail/google-cast/boadgeojelhgndaghljhdicfkmllpafd Ignoring CEC Data ----------------- The Chromecast typically reports whether it is the active input on the device to which it is connected. This value is stored inside a cast object in the following property. .. code:: python cast.status.is_active_input Some Chromecast users have reported CEC incompatibilities with their media center devices. These incompatibilities may sometimes cause this active input value to be reported improperly. This active input value is typically used to determine if the Chromecast is idle. PyChromecast is capable of ignoring the active input value when determining if the Chromecast is idle in the instance that the Chromecast is returning erroneous values. To ignore this CEC detection data in PyChromecast, append a `Linux style wildcard`_ formatted string to the IGNORE\_CEC list in PyChromecast like in the example below. .. code:: python pychromecast.IGNORE_CEC.append('*') # Ignore CEC on all devices pychromecast.IGNORE_CEC.append('Living Room') # Ignore CEC on Chromecasts named Living Room Maintainers ----------- - Jan Borsodi (`@am0s`_) - Ryan Kraus (`@rmkraus`_) - Paulus Schoutsen (`@balloob`_, original author) Thanks ------ I would like to thank `Fred Clift`_ for laying the socket client ground work. Without him it would not have been possible! .. _Linux style wildcard: http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm .. _@am0s: https://github.com/am0s .. _@rmkraus: https://github.com/rmkraus .. _@balloob: https://github.com/balloob .. _Fred Clift: https://github.com/minektur pychromecast-0.8.1/__init__.py000066400000000000000000000000001306355002200163140ustar00rootroot00000000000000pychromecast-0.8.1/chromecast_protobuf/000077500000000000000000000000001306355002200202655ustar00rootroot00000000000000pychromecast-0.8.1/chromecast_protobuf/README.md000066400000000000000000000002351306355002200215440ustar00rootroot00000000000000These files were imported from https://chromium.googlesource.com/chromium/src.git/+/master/extensions/common/api/cast_channel to generate the \_pb2.py-files.pychromecast-0.8.1/chromecast_protobuf/authority_keys.proto000066400000000000000000000006311306355002200244350ustar00rootroot00000000000000// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. syntax = "proto2"; option optimize_for = LITE_RUNTIME; package extensions.api.cast_channel.proto; message AuthorityKeys { message Key { required bytes fingerprint = 1; required bytes public_key = 2; } repeated Key keys = 1; } pychromecast-0.8.1/chromecast_protobuf/cast_channel.proto000066400000000000000000000057001306355002200237760ustar00rootroot00000000000000// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. syntax = "proto2"; option optimize_for = LITE_RUNTIME; package extensions.api.cast_channel; message CastMessage { // Always pass a version of the protocol for future compatibility // requirements. enum ProtocolVersion { CASTV2_1_0 = 0; } required ProtocolVersion protocol_version = 1; // source and destination ids identify the origin and destination of the // message. They are used to route messages between endpoints that share a // device-to-device channel. // // For messages between applications: // - The sender application id is a unique identifier generated on behalf of // the sender application. // - The receiver id is always the the session id for the application. // // For messages to or from the sender or receiver platform, the special ids // 'sender-0' and 'receiver-0' can be used. // // For messages intended for all endpoints using a given channel, the // wildcard destination_id '*' can be used. required string source_id = 2; required string destination_id = 3; // This is the core multiplexing key. All messages are sent on a namespace // and endpoints sharing a channel listen on one or more namespaces. The // namespace defines the protocol and semantics of the message. required string namespace = 4; // Encoding and payload info follows. // What type of data do we have in this message. enum PayloadType { STRING = 0; BINARY = 1; } required PayloadType payload_type = 5; // Depending on payload_type, exactly one of the following optional fields // will always be set. optional string payload_utf8 = 6; optional bytes payload_binary = 7; } enum SignatureAlgorithm { UNSPECIFIED = 0; RSASSA_PKCS1v15 = 1; RSASSA_PSS = 2; } enum HashAlgorithm { SHA1 = 0; SHA256 = 1; } // Messages for authentication protocol between a sender and a receiver. message AuthChallenge { optional SignatureAlgorithm signature_algorithm = 1 [default = RSASSA_PKCS1v15]; optional bytes sender_nonce = 2; optional HashAlgorithm hash_algorithm = 3 [default = SHA1]; } message AuthResponse { required bytes signature = 1; required bytes client_auth_certificate = 2; repeated bytes intermediate_certificate = 3; optional SignatureAlgorithm signature_algorithm = 4 [default = RSASSA_PKCS1v15]; optional bytes sender_nonce = 5; optional HashAlgorithm hash_algorithm = 6 [default = SHA1]; optional bytes crl = 7; } message AuthError { enum ErrorType { INTERNAL_ERROR = 0; NO_TLS = 1; // The underlying connection is not TLS SIGNATURE_ALGORITHM_UNAVAILABLE = 2; } required ErrorType error_type = 1; } message DeviceAuthMessage { // Request fields optional AuthChallenge challenge = 1; // Response fields optional AuthResponse response = 2; optional AuthError error = 3; } pychromecast-0.8.1/chromecast_protobuf/logging.proto000066400000000000000000000120521306355002200230000ustar00rootroot00000000000000// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. syntax = "proto2"; option optimize_for = LITE_RUNTIME; package extensions.api.cast_channel.proto; enum EventType { EVENT_TYPE_UNKNOWN = 0; CAST_SOCKET_CREATED = 1; READY_STATE_CHANGED = 2; CONNECTION_STATE_CHANGED = 3; READ_STATE_CHANGED = 4; WRITE_STATE_CHANGED = 5; ERROR_STATE_CHANGED = 6; CONNECT_FAILED = 7; TCP_SOCKET_CONNECT = 8; // Logged with RV. TCP_SOCKET_SET_KEEP_ALIVE = 9; SSL_CERT_WHITELISTED = 10; SSL_SOCKET_CONNECT = 11; // Logged with RV. SSL_INFO_OBTAINED = 12; DER_ENCODED_CERT_OBTAIN = 13; // Logged with RV. RECEIVED_CHALLENGE_REPLY = 14; AUTH_CHALLENGE_REPLY = 15; CONNECT_TIMED_OUT = 16; SEND_MESSAGE_FAILED = 17; MESSAGE_ENQUEUED = 18; // Message SOCKET_WRITE = 19; // Logged with RV. MESSAGE_WRITTEN = 20; // Message SOCKET_READ = 21; // Logged with RV. MESSAGE_READ = 22; // Message SOCKET_CLOSED = 25; SSL_CERT_EXCESSIVE_LIFETIME = 26; CHANNEL_POLICY_ENFORCED = 27; TCP_SOCKET_CONNECT_COMPLETE = 28; // Logged with RV. SSL_SOCKET_CONNECT_COMPLETE = 29; // Logged with RV. SSL_SOCKET_CONNECT_FAILED = 30; // Logged with RV. SEND_AUTH_CHALLENGE_FAILED = 31; // Logged with RV. AUTH_CHALLENGE_REPLY_INVALID = 32; PING_WRITE_ERROR = 33; // Logged with RV. } enum ChannelAuth { // SSL over TCP. SSL = 1; // SSL over TCP with challenge and receiver signature verification. SSL_VERIFIED = 2; } enum ReadyState { READY_STATE_NONE = 1; READY_STATE_CONNECTING = 2; READY_STATE_OPEN = 3; READY_STATE_CLOSING = 4; READY_STATE_CLOSED = 5; } enum ConnectionState { CONN_STATE_UNKNOWN = 1; CONN_STATE_TCP_CONNECT = 2; CONN_STATE_TCP_CONNECT_COMPLETE = 3; CONN_STATE_SSL_CONNECT = 4; CONN_STATE_SSL_CONNECT_COMPLETE = 5; CONN_STATE_AUTH_CHALLENGE_SEND = 6; CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE = 7; CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE = 8; CONN_STATE_START_CONNECT = 9; // Terminal states follow. CONN_STATE_FINISHED = 100; CONN_STATE_ERROR = 101; CONN_STATE_TIMEOUT = 102; } enum ReadState { READ_STATE_UNKNOWN = 1; READ_STATE_READ = 2; READ_STATE_READ_COMPLETE = 3; READ_STATE_DO_CALLBACK = 4; READ_STATE_HANDLE_ERROR = 5; READ_STATE_ERROR = 100; // Terminal state. } enum WriteState { WRITE_STATE_UNKNOWN = 1; WRITE_STATE_WRITE = 2; WRITE_STATE_WRITE_COMPLETE = 3; WRITE_STATE_DO_CALLBACK = 4; WRITE_STATE_HANDLE_ERROR = 5; // Terminal states follow. WRITE_STATE_ERROR = 100; WRITE_STATE_IDLE = 101; } enum ErrorState { CHANNEL_ERROR_NONE = 1; CHANNEL_ERROR_CHANNEL_NOT_OPEN = 2; CHANNEL_ERROR_AUTHENTICATION_ERROR = 3; CHANNEL_ERROR_CONNECT_ERROR = 4; CHANNEL_ERROR_SOCKET_ERROR = 5; CHANNEL_ERROR_TRANSPORT_ERROR = 6; CHANNEL_ERROR_INVALID_MESSAGE = 7; CHANNEL_ERROR_INVALID_CHANNEL_ID = 8; CHANNEL_ERROR_CONNECT_TIMEOUT = 9; CHANNEL_ERROR_UNKNOWN = 10; } enum ChallengeReplyErrorType { CHALLENGE_REPLY_ERROR_NONE = 1; CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY = 2; CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE = 3; CHALLENGE_REPLY_ERROR_NO_PAYLOAD = 4; CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED = 5; CHALLENGE_REPLY_ERROR_MESSAGE_ERROR = 6; CHALLENGE_REPLY_ERROR_NO_RESPONSE = 7; CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND = 8; CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED = 9; CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA = 10; CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY = 11; CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH = 12; CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG = 13; CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE = 14; CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED = 15; CHALLENGE_REPLY_ERROR_CRL_INVALID = 16; CHALLENGE_REPLY_ERROR_CERT_REVOKED = 17; } message SocketEvent { // Required optional EventType type = 1; optional int64 timestamp_micros = 2; optional string details = 3; optional int32 net_return_value = 4; optional string message_namespace = 5; optional ReadyState ready_state = 6; optional ConnectionState connection_state = 7; optional ReadState read_state = 8; optional WriteState write_state = 9; optional ErrorState error_state = 10; optional ChallengeReplyErrorType challenge_reply_error_type = 11; // No longer used. optional int32 nss_error_code = 12; } message AggregatedSocketEvent { optional int32 id = 1; optional int32 endpoint_id = 2; optional ChannelAuth channel_auth_type = 3; repeated SocketEvent socket_event = 4; optional int64 bytes_read = 5; optional int64 bytes_written = 6; } message Log { // Each AggregatedSocketEvent represents events recorded for a socket. repeated AggregatedSocketEvent aggregated_socket_event = 1; // Number of socket log entries evicted by the logger due to size constraints. optional int32 num_evicted_aggregated_socket_events = 2; // Number of event log entries evicted by the logger due to size constraints. optional int32 num_evicted_socket_events = 3; } pychromecast-0.8.1/examples/000077500000000000000000000000001306355002200160335ustar00rootroot00000000000000pychromecast-0.8.1/examples/blocking.py000066400000000000000000000034041306355002200201760ustar00rootroot00000000000000""" Example that shows how the new Python 2 socket client can be used. Functions called in this example are blocking which means that the function doesn't return as long as no result was received. """ from __future__ import print_function import time import sys import logging import pychromecast import pychromecast.controllers.youtube as youtube if '--show-debug' in sys.argv: logging.basicConfig(level=logging.DEBUG) casts = pychromecast.get_chromecasts() if len(casts) == 0: print("No Devices Found") exit() cast = casts[0] yt = youtube.YouTubeController() cast.register_handler(yt) print() print(cast.device) time.sleep(1) print() print(cast.status) print() print(cast.media_controller.status) print() if '--show-status-only' in sys.argv: sys.exit() if not cast.is_idle: print("Killing current running app") cast.quit_app() time.sleep(5) print("Playing media") cast.play_media( ("http://commondatastorage.googleapis.com/gtv-videos-bucket/" "sample/BigBuckBunny.mp4"), "video/mp4") t = 0 while True: try: t += 1 if t > 10 and t % 3 == 0: print("Media status", cast.media_controller.status) if t == 15: print("Sending pause command") cast.media_controller.pause() elif t == 20: print("Sending play command") cast.media_controller.play() elif t == 25: print("Sending stop command") cast.media_controller.stop() elif t == 27: print("Switching to YouTube") yt.play_video("L0MK7qz13bU") elif t == 38: cast.media_controller.pause() elif t == 45: cast.quit_app() break time.sleep(1) except KeyboardInterrupt: break pychromecast-0.8.1/examples/non_blocking.py000066400000000000000000000043211306355002200210470ustar00rootroot00000000000000""" Example that shows how the new Python 2 socket client can be used. All functions (except get_chromecast()) are non-blocking and return immediately without waiting for the result. You can use that functionality to include pychromecast into your main loop. """ from __future__ import print_function import time import select import sys import logging import pychromecast """ Check for cast.socket_client.get_socket() and handle it with cast.socket_client.run_once() """ def your_main_loop(): t = 1 cast = None def callback(chromecast): nonlocal cast cast = chromecast stop_discovery() stop_discovery = pychromecast.get_chromecasts(blocking=False, callback=callback) while True: if cast: polltime = 0.1 can_read, _, _ = select.select([cast.socket_client.get_socket()], [], [], polltime) if can_read: #received something on the socket, handle it with run_once() cast.socket_client.run_once() do_actions(cast, t) t += 1 if(t > 50): break else: print("=> Waiting for cast discovery...") time.sleep(1) """ Your code which is called by main loop """ def do_actions(cast, t): if t == 5: print() print("=> Sending non-blocking play_media command") cast.play_media( ("http://commondatastorage.googleapis.com/gtv-videos-bucket/" "sample/BigBuckBunny.mp4"), "video/mp4") elif t == 30: print() print("=> Sending non-blocking pause command") cast.media_controller.pause() elif t == 35: print() print("=> Sending non-blocking play command") cast.media_controller.play() elif t == 40: print() print("=> Sending non-blocking stop command") cast.media_controller.stop() elif t == 45: print() print("=> Sending non-blocking quit_app command") cast.quit_app() elif t % 4 == 0: print() print("Media status", cast.media_controller.status) if '--show-debug' in sys.argv: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) your_main_loop() pychromecast-0.8.1/fabfile.py000066400000000000000000000010131306355002200161520ustar00rootroot00000000000000import os from fabric.decorators import task from fabric.operations import local @task def build(): """ Builds the distribution files """ if not os.path.exists("build"): os.mkdir("build") local("date >> build/log") local("python setup.py sdist >> build/log") local("python setup.py bdist_wheel >> build/log") @task def release(): """ Uploads files to PyPi to create a new release. Note: Requires that files have been built first """ local("twine upload dist/*") pychromecast-0.8.1/pychromecast/000077500000000000000000000000001306355002200167165ustar00rootroot00000000000000pychromecast-0.8.1/pychromecast/__init__.py000066400000000000000000000320431306355002200210310ustar00rootroot00000000000000""" PyChromecast: remote control your Chromecast """ from __future__ import print_function import sys import logging import fnmatch # pylint: disable=wildcard-import import threading from .config import * # noqa from .error import * # noqa from . import socket_client from .discovery import discover_chromecasts, start_discovery, stop_discovery from .dial import get_device_status, reboot, DeviceStatus, CAST_TYPES, \ CAST_TYPE_CHROMECAST from .controllers.media import STREAM_TYPE_BUFFERED # noqa __all__ = ( '__version__', '__version_info__', 'get_chromecasts', 'Chromecast', ) __version_info__ = ('0', '7', '6') __version__ = '.'.join(__version_info__) IDLE_APP_ID = 'E8C28D3C' IGNORE_CEC = [] # For Python 2.x we need to decode __repr__ Unicode return values to str NON_UNICODE_REPR = sys.version_info < (3, ) def _get_chromecast_from_host(host, tries=None, retry_wait=None, timeout=None, blocking=True): """Creates a Chromecast object from a zeroconf host.""" # Build device status from the mDNS info, this information is # the primary source and the remaining will be fetched # later on. ip_address, port, uuid, model_name, friendly_name = host cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST) device = DeviceStatus( friendly_name=friendly_name, model_name=model_name, manufacturer=None, api_version=None, uuid=uuid, cast_type=cast_type, ) return Chromecast(host=ip_address, port=port, device=device, tries=tries, timeout=timeout, retry_wait=retry_wait, blocking=blocking) # pylint: disable=too-many-locals def get_chromecasts(tries=None, retry_wait=None, timeout=None, blocking=True, callback=None): """ Searches the network for chromecast devices. If blocking = True, returns a list of discovered chromecast devices. If blocking = False, triggers a callback for each discovered chromecast, and returns a function which can be executed to stop discovery. ex: get_chromecasts(friendly_name="Living Room") May return an empty list if no chromecasts were found. Tries is specified if you want to limit the number of times the underlying socket associated with your Chromecast objects will retry connecting if connection is lost or it fails to connect in the first place. The number of seconds spent between each retry can be defined by passing the retry_wait parameter, the default is to wait 5 seconds. """ if blocking: # Thread blocking chromecast discovery hosts = discover_chromecasts() cc_list = [] for host in hosts: try: cc_list.append(_get_chromecast_from_host( host, tries=tries, retry_wait=retry_wait, timeout=timeout, blocking=blocking)) except ChromecastConnectionError: # noqa pass return cc_list else: # Callback based chromecast discovery if not callable(callback): raise ValueError( "Nonblocking discovery requires a callback function.") def internal_callback(name): """Called when zeroconf has discovered a new chromecast.""" try: callback(_get_chromecast_from_host( listener.services[name], tries=tries, retry_wait=retry_wait, timeout=timeout, blocking=blocking)) except ChromecastConnectionError: # noqa pass def internal_stop(): """Stops discovery of new chromecasts.""" stop_discovery(browser) listener, browser = start_discovery(internal_callback) return internal_stop # pylint: disable=too-many-instance-attributes class Chromecast(object): """ Class to interface with a ChromeCast. :param port: The port to use when connecting to the device, set to None to use the default of 8009. Special devices such as Cast Groups may return a different port number so we need to use that. :param device: DeviceStatus with initial information for the device. :type device: pychromecast.dial.DeviceStatus :param tries: Number of retries to perform if the connection fails. None for inifinite retries. :param timeout: A floating point number specifying the socket timeout in seconds. None means to use the default which is 30 seconds. :param retry_wait: A floating point number specifying how many seconds to wait between each retry. None means to use the default which is 5 seconds. """ def __init__(self, host, port=None, device=None, **kwargs): tries = kwargs.pop('tries', None) timeout = kwargs.pop('timeout', None) retry_wait = kwargs.pop('retry_wait', None) blocking = kwargs.pop('blocking', True) self.logger = logging.getLogger(__name__) # Resolve host to IP address self.host = host self.port = port or 8009 self.logger.info("Querying device status") self.device = device if device: dev_status = get_device_status(self.host) if dev_status: # Values from `device` have priority over `dev_status` # as they come from the dial information. # `dev_status` may add extra information such as `manufacturer` # which dial does not supply self.device = DeviceStatus( friendly_name=(device.friendly_name or dev_status.friendly_name), model_name=(device.model_name or dev_status.model_name), manufacturer=(device.manufacturer or dev_status.manufacturer), api_version=(device.api_version or dev_status.api_version), uuid=(device.uuid or dev_status.uuid), cast_type=(device.cast_type or dev_status.cast_type), ) else: self.device = device else: self.device = get_device_status(self.host) if not self.device: raise ChromecastConnectionError( # noqa "Could not connect to {}:{}".format(self.host, self.port)) self.status = None self.status_event = threading.Event() self.socket_client = socket_client.SocketClient( host, port=port, cast_type=self.device.cast_type, tries=tries, timeout=timeout, retry_wait=retry_wait, blocking=blocking) receiver_controller = self.socket_client.receiver_controller receiver_controller.register_status_listener(self) # Forward these methods self.set_volume = receiver_controller.set_volume self.set_volume_muted = receiver_controller.set_volume_muted self.play_media = self.socket_client.media_controller.play_media self.register_handler = self.socket_client.register_handler self.register_status_listener = \ receiver_controller.register_status_listener self.register_launch_error_listener = \ receiver_controller.register_launch_error_listener self.register_connection_listener = \ self.socket_client.register_connection_listener if blocking: self.socket_client.start() @property def ignore_cec(self): """ Returns whether the CEC data should be ignored. """ return self.device is not None and \ any([fnmatch.fnmatchcase(self.device.friendly_name, pattern) for pattern in IGNORE_CEC]) @property def is_idle(self): """ Returns if there is currently an app running. """ return (self.status is None or self.app_id in (None, IDLE_APP_ID) or (not self.status.is_active_input and not self.ignore_cec)) @property def uuid(self): """ Returns the unique UUID of the Chromecast device. """ return self.device.uuid @property def name(self): """ Returns the friendly name set for the Chromecast device. This is the name that the end-user chooses for the cast device. """ return self.device.friendly_name @property def model_name(self): """ Returns the model name of the Chromecast device. """ return self.device.model_name @property def cast_type(self): """ Returns the type of the Chromecast device. This is one of CAST_TYPE_CHROMECAST for regular Chromecast device, CAST_TYPE_AUDIO for Chromecast devices that only support audio and CAST_TYPE_GROUP for virtual a Chromecast device that groups together two or more cast (Audio for now) devices. :rtype: str """ return self.device.cast_type @property def app_id(self): """ Returns the current app_id. """ return self.status.app_id if self.status else None @property def app_display_name(self): """ Returns the name of the current running app. """ return self.status.display_name if self.status else None @property def media_controller(self): """ Returns the media controller. """ return self.socket_client.media_controller def new_cast_status(self, status): """ Called when a new status received from the Chromecast. """ self.status = status if status: self.status_event.set() def start_app(self, app_id): """ Start an app on the Chromecast. """ self.logger.info("Starting app %s", app_id) self.socket_client.receiver_controller.launch_app(app_id) def quit_app(self): """ Tells the Chromecast to quit current app_id. """ self.logger.info("Quiting current app") self.socket_client.receiver_controller.stop_app() def reboot(self): """ Reboots the Chromecast. """ reboot(self.host) def volume_up(self, delta=0.1): """ Increment volume by 0.1 (or delta) unless it is already maxed. Returns the new volume. """ if delta <= 0: raise ValueError( "volume delta must be greater than zero, not {}".format(delta)) return self.set_volume(self.status.volume_level + delta) def volume_down(self, delta=0.1): """ Decrement the volume by 0.1 (or delta) unless it is already 0. Returns the new volume. """ if delta <= 0: raise ValueError( "volume delta must be greater than zero, not {}".format(delta)) return self.set_volume(self.status.volume_level - delta) def wait(self, timeout=None): """ Waits until the cast device is ready for communication. The device is ready as soon a status message has been received. If the status has already been received then the method returns immediately. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. """ self.status_event.wait(timeout=timeout) def disconnect(self, timeout=None, blocking=True): """ Disconnects the chromecast and waits for it to terminate. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. :param blocking: If True it will block until the disconnection is complete, otherwise it will return immediately. """ self.socket_client.disconnect() if blocking: self.join(timeout=timeout) def join(self, timeout=None): """ Blocks the thread of the caller until the chromecast connection is stopped. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. """ self.socket_client.join(timeout=timeout) def __del__(self): try: self.socket_client.stop.set() except AttributeError: pass def __repr__(self): txt = u"Chromecast({!r}, port={!r}, device={!r})".format( self.host, self.port, self.device) # Python 2.x does not work well with unicode returned from repr if NON_UNICODE_REPR: return txt.encode('utf-8') return txt def __unicode__(self): return u"Chromecast({}, {}, {}, {}, {}, api={}.{})".format( self.host, self.port, self.device.friendly_name, self.device.model_name, self.device.manufacturer, self.device.api_version[0], self.device.api_version[1]) pychromecast-0.8.1/pychromecast/authority_keys_pb2.py000066400000000000000000000075421306355002200231260ustar00rootroot00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: authority_keys.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='authority_keys.proto', package='extensions.api.cast_channel.proto', syntax='proto2', serialized_pb=_b('\n\x14\x61uthority_keys.proto\x12!extensions.api.cast_channel.proto\"\x83\x01\n\rAuthorityKeys\x12\x42\n\x04keys\x18\x01 \x03(\x0b\x32\x34.extensions.api.cast_channel.proto.AuthorityKeys.Key\x1a.\n\x03Key\x12\x13\n\x0b\x66ingerprint\x18\x01 \x02(\x0c\x12\x12\n\npublic_key\x18\x02 \x02(\x0c\x42\x02H\x03') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _AUTHORITYKEYS_KEY = _descriptor.Descriptor( name='Key', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='fingerprint', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key.fingerprint', index=0, number=1, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='public_key', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key.public_key', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=145, serialized_end=191, ) _AUTHORITYKEYS = _descriptor.Descriptor( name='AuthorityKeys', full_name='extensions.api.cast_channel.proto.AuthorityKeys', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='keys', full_name='extensions.api.cast_channel.proto.AuthorityKeys.keys', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[_AUTHORITYKEYS_KEY, ], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=60, serialized_end=191, ) _AUTHORITYKEYS_KEY.containing_type = _AUTHORITYKEYS _AUTHORITYKEYS.fields_by_name['keys'].message_type = _AUTHORITYKEYS_KEY DESCRIPTOR.message_types_by_name['AuthorityKeys'] = _AUTHORITYKEYS AuthorityKeys = _reflection.GeneratedProtocolMessageType('AuthorityKeys', (_message.Message,), dict( Key = _reflection.GeneratedProtocolMessageType('Key', (_message.Message,), dict( DESCRIPTOR = _AUTHORITYKEYS_KEY, __module__ = 'authority_keys_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AuthorityKeys.Key) )) , DESCRIPTOR = _AUTHORITYKEYS, __module__ = 'authority_keys_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AuthorityKeys) )) _sym_db.RegisterMessage(AuthorityKeys) _sym_db.RegisterMessage(AuthorityKeys.Key) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003')) # @@protoc_insertion_point(module_scope) pychromecast-0.8.1/pychromecast/cast_channel_pb2.py000066400000000000000000000451571306355002200224710ustar00rootroot00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: cast_channel.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='cast_channel.proto', package='extensions.api.cast_channel', syntax='proto2', serialized_pb=_b('\n\x12\x63\x61st_channel.proto\x12\x1b\x65xtensions.api.cast_channel\"\xe3\x02\n\x0b\x43\x61stMessage\x12R\n\x10protocol_version\x18\x01 \x02(\x0e\x32\x38.extensions.api.cast_channel.CastMessage.ProtocolVersion\x12\x11\n\tsource_id\x18\x02 \x02(\t\x12\x16\n\x0e\x64\x65stination_id\x18\x03 \x02(\t\x12\x11\n\tnamespace\x18\x04 \x02(\t\x12J\n\x0cpayload_type\x18\x05 \x02(\x0e\x32\x34.extensions.api.cast_channel.CastMessage.PayloadType\x12\x14\n\x0cpayload_utf8\x18\x06 \x01(\t\x12\x16\n\x0epayload_binary\x18\x07 \x01(\x0c\"!\n\x0fProtocolVersion\x12\x0e\n\nCASTV2_1_0\x10\x00\"%\n\x0bPayloadType\x12\n\n\x06STRING\x10\x00\x12\n\n\x06\x42INARY\x10\x01\"\xce\x01\n\rAuthChallenge\x12]\n\x13signature_algorithm\x18\x01 \x01(\x0e\x32/.extensions.api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\x12\x14\n\x0csender_nonce\x18\x02 \x01(\x0c\x12H\n\x0ehash_algorithm\x18\x03 \x01(\x0e\x32*.extensions.api.cast_channel.HashAlgorithm:\x04SHA1\"\xb0\x02\n\x0c\x41uthResponse\x12\x11\n\tsignature\x18\x01 \x02(\x0c\x12\x1f\n\x17\x63lient_auth_certificate\x18\x02 \x02(\x0c\x12 \n\x18intermediate_certificate\x18\x03 \x03(\x0c\x12]\n\x13signature_algorithm\x18\x04 \x01(\x0e\x32/.extensions.api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\x12\x14\n\x0csender_nonce\x18\x05 \x01(\x0c\x12H\n\x0ehash_algorithm\x18\x06 \x01(\x0e\x32*.extensions.api.cast_channel.HashAlgorithm:\x04SHA1\x12\x0b\n\x03\x63rl\x18\x07 \x01(\x0c\"\xa3\x01\n\tAuthError\x12\x44\n\nerror_type\x18\x01 \x02(\x0e\x32\x30.extensions.api.cast_channel.AuthError.ErrorType\"P\n\tErrorType\x12\x12\n\x0eINTERNAL_ERROR\x10\x00\x12\n\n\x06NO_TLS\x10\x01\x12#\n\x1fSIGNATURE_ALGORITHM_UNAVAILABLE\x10\x02\"\xc6\x01\n\x11\x44\x65viceAuthMessage\x12=\n\tchallenge\x18\x01 \x01(\x0b\x32*.extensions.api.cast_channel.AuthChallenge\x12;\n\x08response\x18\x02 \x01(\x0b\x32).extensions.api.cast_channel.AuthResponse\x12\x35\n\x05\x65rror\x18\x03 \x01(\x0b\x32&.extensions.api.cast_channel.AuthError*J\n\x12SignatureAlgorithm\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x13\n\x0fRSASSA_PKCS1v15\x10\x01\x12\x0e\n\nRSASSA_PSS\x10\x02*%\n\rHashAlgorithm\x12\x08\n\x04SHA1\x10\x00\x12\n\n\x06SHA256\x10\x01\x42\x02H\x03') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _SIGNATUREALGORITHM = _descriptor.EnumDescriptor( name='SignatureAlgorithm', full_name='extensions.api.cast_channel.SignatureAlgorithm', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='UNSPECIFIED', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='RSASSA_PKCS1v15', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='RSASSA_PSS', index=2, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1292, serialized_end=1366, ) _sym_db.RegisterEnumDescriptor(_SIGNATUREALGORITHM) SignatureAlgorithm = enum_type_wrapper.EnumTypeWrapper(_SIGNATUREALGORITHM) _HASHALGORITHM = _descriptor.EnumDescriptor( name='HashAlgorithm', full_name='extensions.api.cast_channel.HashAlgorithm', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SHA1', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='SHA256', index=1, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=1368, serialized_end=1405, ) _sym_db.RegisterEnumDescriptor(_HASHALGORITHM) HashAlgorithm = enum_type_wrapper.EnumTypeWrapper(_HASHALGORITHM) UNSPECIFIED = 0 RSASSA_PKCS1v15 = 1 RSASSA_PSS = 2 SHA1 = 0 SHA256 = 1 _CASTMESSAGE_PROTOCOLVERSION = _descriptor.EnumDescriptor( name='ProtocolVersion', full_name='extensions.api.cast_channel.CastMessage.ProtocolVersion', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CASTV2_1_0', index=0, number=0, options=None, type=None), ], containing_type=None, options=None, serialized_start=335, serialized_end=368, ) _sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PROTOCOLVERSION) _CASTMESSAGE_PAYLOADTYPE = _descriptor.EnumDescriptor( name='PayloadType', full_name='extensions.api.cast_channel.CastMessage.PayloadType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='STRING', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='BINARY', index=1, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=370, serialized_end=407, ) _sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PAYLOADTYPE) _AUTHERROR_ERRORTYPE = _descriptor.EnumDescriptor( name='ErrorType', full_name='extensions.api.cast_channel.AuthError.ErrorType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='INTERNAL_ERROR', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='NO_TLS', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='SIGNATURE_ALGORITHM_UNAVAILABLE', index=2, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1009, serialized_end=1089, ) _sym_db.RegisterEnumDescriptor(_AUTHERROR_ERRORTYPE) _CASTMESSAGE = _descriptor.Descriptor( name='CastMessage', full_name='extensions.api.cast_channel.CastMessage', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='protocol_version', full_name='extensions.api.cast_channel.CastMessage.protocol_version', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='source_id', full_name='extensions.api.cast_channel.CastMessage.source_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='destination_id', full_name='extensions.api.cast_channel.CastMessage.destination_id', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='namespace', full_name='extensions.api.cast_channel.CastMessage.namespace', index=3, number=4, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload_type', full_name='extensions.api.cast_channel.CastMessage.payload_type', index=4, number=5, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload_utf8', full_name='extensions.api.cast_channel.CastMessage.payload_utf8', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload_binary', full_name='extensions.api.cast_channel.CastMessage.payload_binary', index=6, number=7, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _CASTMESSAGE_PROTOCOLVERSION, _CASTMESSAGE_PAYLOADTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=52, serialized_end=407, ) _AUTHCHALLENGE = _descriptor.Descriptor( name='AuthChallenge', full_name='extensions.api.cast_channel.AuthChallenge', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='signature_algorithm', full_name='extensions.api.cast_channel.AuthChallenge.signature_algorithm', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=True, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sender_nonce', full_name='extensions.api.cast_channel.AuthChallenge.sender_nonce', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='hash_algorithm', full_name='extensions.api.cast_channel.AuthChallenge.hash_algorithm', index=2, number=3, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=410, serialized_end=616, ) _AUTHRESPONSE = _descriptor.Descriptor( name='AuthResponse', full_name='extensions.api.cast_channel.AuthResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='signature', full_name='extensions.api.cast_channel.AuthResponse.signature', index=0, number=1, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_auth_certificate', full_name='extensions.api.cast_channel.AuthResponse.client_auth_certificate', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='intermediate_certificate', full_name='extensions.api.cast_channel.AuthResponse.intermediate_certificate', index=2, number=3, type=12, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='signature_algorithm', full_name='extensions.api.cast_channel.AuthResponse.signature_algorithm', index=3, number=4, type=14, cpp_type=8, label=1, has_default_value=True, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sender_nonce', full_name='extensions.api.cast_channel.AuthResponse.sender_nonce', index=4, number=5, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='hash_algorithm', full_name='extensions.api.cast_channel.AuthResponse.hash_algorithm', index=5, number=6, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='crl', full_name='extensions.api.cast_channel.AuthResponse.crl', index=6, number=7, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=619, serialized_end=923, ) _AUTHERROR = _descriptor.Descriptor( name='AuthError', full_name='extensions.api.cast_channel.AuthError', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='error_type', full_name='extensions.api.cast_channel.AuthError.error_type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _AUTHERROR_ERRORTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=926, serialized_end=1089, ) _DEVICEAUTHMESSAGE = _descriptor.Descriptor( name='DeviceAuthMessage', full_name='extensions.api.cast_channel.DeviceAuthMessage', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='challenge', full_name='extensions.api.cast_channel.DeviceAuthMessage.challenge', index=0, number=1, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='response', full_name='extensions.api.cast_channel.DeviceAuthMessage.response', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='error', full_name='extensions.api.cast_channel.DeviceAuthMessage.error', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1092, serialized_end=1290, ) _CASTMESSAGE.fields_by_name['protocol_version'].enum_type = _CASTMESSAGE_PROTOCOLVERSION _CASTMESSAGE.fields_by_name['payload_type'].enum_type = _CASTMESSAGE_PAYLOADTYPE _CASTMESSAGE_PROTOCOLVERSION.containing_type = _CASTMESSAGE _CASTMESSAGE_PAYLOADTYPE.containing_type = _CASTMESSAGE _AUTHCHALLENGE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM _AUTHCHALLENGE.fields_by_name['hash_algorithm'].enum_type = _HASHALGORITHM _AUTHRESPONSE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM _AUTHRESPONSE.fields_by_name['hash_algorithm'].enum_type = _HASHALGORITHM _AUTHERROR.fields_by_name['error_type'].enum_type = _AUTHERROR_ERRORTYPE _AUTHERROR_ERRORTYPE.containing_type = _AUTHERROR _DEVICEAUTHMESSAGE.fields_by_name['challenge'].message_type = _AUTHCHALLENGE _DEVICEAUTHMESSAGE.fields_by_name['response'].message_type = _AUTHRESPONSE _DEVICEAUTHMESSAGE.fields_by_name['error'].message_type = _AUTHERROR DESCRIPTOR.message_types_by_name['CastMessage'] = _CASTMESSAGE DESCRIPTOR.message_types_by_name['AuthChallenge'] = _AUTHCHALLENGE DESCRIPTOR.message_types_by_name['AuthResponse'] = _AUTHRESPONSE DESCRIPTOR.message_types_by_name['AuthError'] = _AUTHERROR DESCRIPTOR.message_types_by_name['DeviceAuthMessage'] = _DEVICEAUTHMESSAGE DESCRIPTOR.enum_types_by_name['SignatureAlgorithm'] = _SIGNATUREALGORITHM DESCRIPTOR.enum_types_by_name['HashAlgorithm'] = _HASHALGORITHM CastMessage = _reflection.GeneratedProtocolMessageType('CastMessage', (_message.Message,), dict( DESCRIPTOR = _CASTMESSAGE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.CastMessage) )) _sym_db.RegisterMessage(CastMessage) AuthChallenge = _reflection.GeneratedProtocolMessageType('AuthChallenge', (_message.Message,), dict( DESCRIPTOR = _AUTHCHALLENGE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthChallenge) )) _sym_db.RegisterMessage(AuthChallenge) AuthResponse = _reflection.GeneratedProtocolMessageType('AuthResponse', (_message.Message,), dict( DESCRIPTOR = _AUTHRESPONSE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthResponse) )) _sym_db.RegisterMessage(AuthResponse) AuthError = _reflection.GeneratedProtocolMessageType('AuthError', (_message.Message,), dict( DESCRIPTOR = _AUTHERROR, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthError) )) _sym_db.RegisterMessage(AuthError) DeviceAuthMessage = _reflection.GeneratedProtocolMessageType('DeviceAuthMessage', (_message.Message,), dict( DESCRIPTOR = _DEVICEAUTHMESSAGE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.DeviceAuthMessage) )) _sym_db.RegisterMessage(DeviceAuthMessage) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003')) # @@protoc_insertion_point(module_scope) pychromecast-0.8.1/pychromecast/config.py000066400000000000000000000017641306355002200205450ustar00rootroot00000000000000""" Data and methods to retrieve app specific configuration """ import json import requests APP_BACKDROP = "E8C28D3C" APP_YOUTUBE = "YouTube" APP_MEDIA_RECEIVER = "CC1AD845" APP_PLEX = "06ee44ee-e7e3-4249-83b6-f5d0b6f07f34_1" def get_possible_app_ids(): """ Returns all possible app ids. """ try: req = requests.get( "https://clients3.google.com/cast/chromecast/device/baseconfig") data = json.loads(req.text[4:]) return [app['app_id'] for app in data['applications']] + \ data["enabled_app_ids"] except ValueError: # If json fails to parse return [] def get_app_config(app_id): """ Get specific configuration for 'app_id'. """ try: req = requests.get( ("https://clients3.google.com/" "cast/chromecast/device/app?a={}").format(app_id)) return json.loads(req.text[4:]) if req.status_code == 200 else {} except ValueError: # If json fails to parse return {} pychromecast-0.8.1/pychromecast/controllers/000077500000000000000000000000001306355002200212645ustar00rootroot00000000000000pychromecast-0.8.1/pychromecast/controllers/__init__.py000066400000000000000000000071051306355002200234000ustar00rootroot00000000000000""" Provides controllers to handle specific namespaces in Chromecast communication. """ import logging from ..error import UnsupportedNamespace, ControllerNotRegistered class BaseController(object): """ ABC for namespace controllers. """ def __init__(self, namespace, supporting_app_id=None, target_platform=False): """ Initialize the controller. namespace: the namespace this controller will act on supporting_app_id: app to be launched if app is running with unsupported namespace. target_platform: set to True if you target the platform instead of current app. """ self.namespace = namespace self.supporting_app_id = supporting_app_id self.target_platform = target_platform self._socket_client = None self._message_func = None self.logger = logging.getLogger(__name__) @property def is_active(self): """ True if the controller is connected to a socket client and the Chromecast is running an app that supports this controller. """ return (self._socket_client is not None and self.namespace in self._socket_client.app_namespaces) def launch(self, callback_function=None): """ If set, launches app related to the controller. """ self._check_registered() self._socket_client.receiver_controller.launch_app( self.supporting_app_id, callback_function=callback_function) def registered(self, socket_client): """ Called when a controller is registered. """ self._socket_client = socket_client if self.target_platform: self._message_func = self._socket_client.send_platform_message else: self._message_func = self._socket_client.send_app_message def channel_connected(self): """ Called when a channel has been openend that supports the namespace of this controller. """ pass def channel_disconnected(self): """ Called when a channel is disconnected. """ pass def send_message(self, data, inc_session_id=False, callback_function=None): """ Send a message on this namespace to the Chromecast. Will raise a NotConnected exception if not connected. """ self._check_registered() if not self.target_platform and \ self.namespace not in self._socket_client.app_namespaces: if self.supporting_app_id is not None: self.launch() else: raise UnsupportedNamespace( ("Namespace {} is not supported by running" "application.").format(self.namespace)) return self._message_func( self.namespace, data, inc_session_id, callback_function) # pylint: disable=unused-argument,no-self-use def receive_message(self, message, data): """ Called when a message is received that matches the namespace. Returns boolean indicating if message was handled. """ return False def tear_down(self): """ Called when we are shutting down. """ self._socket_client = None self._message_func = None def _check_registered(self): """ Helper method to see if we are registered with a Cast object. """ if self._socket_client is None: raise ControllerNotRegistered(( "Trying to use the controller without it being registered " "with a Cast object.")) pychromecast-0.8.1/pychromecast/controllers/media.py000066400000000000000000000442151306355002200227230ustar00rootroot00000000000000""" Provides a controller for controlling the default media players on the Chromecast. """ from collections import namedtuple import threading from ..config import APP_MEDIA_RECEIVER from . import BaseController STREAM_TYPE_UNKNOWN = "UNKNOWN" STREAM_TYPE_BUFFERED = "BUFFERED" STREAM_TYPE_LIVE = "LIVE" MEDIA_PLAYER_STATE_PLAYING = "PLAYING" MEDIA_PLAYER_STATE_BUFFERING = "BUFFERING" MEDIA_PLAYER_STATE_PAUSED = "PAUSED" MEDIA_PLAYER_STATE_IDLE = "IDLE" MEDIA_PLAYER_STATE_UNKNOWN = "UNKNOWN" MESSAGE_TYPE = 'type' TYPE_GET_STATUS = "GET_STATUS" TYPE_MEDIA_STATUS = "MEDIA_STATUS" TYPE_PLAY = "PLAY" TYPE_PAUSE = "PAUSE" TYPE_STOP = "STOP" TYPE_LOAD = "LOAD" TYPE_SEEK = "SEEK" TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO" METADATA_TYPE_GENERIC = 0 METADATA_TYPE_TVSHOW = 1 METADATA_TYPE_MOVIE = 2 METADATA_TYPE_MUSICTRACK = 3 METADATA_TYPE_PHOTO = 4 CMD_SUPPORT_PAUSE = 1 CMD_SUPPORT_SEEK = 2 CMD_SUPPORT_STREAM_VOLUME = 4 CMD_SUPPORT_STREAM_MUTE = 8 CMD_SUPPORT_SKIP_FORWARD = 16 CMD_SUPPORT_SKIP_BACKWARD = 32 MediaImage = namedtuple('MediaImage', 'url height width') class MediaStatus(object): """ Class to hold the media status. """ # pylint: disable=too-many-instance-attributes,too-many-public-methods def __init__(self): self.current_time = 0 self.content_id = None self.content_type = None self.duration = None self.stream_type = STREAM_TYPE_UNKNOWN self.idle_reason = None self.media_session_id = None self.playback_rate = 1 self.player_state = MEDIA_PLAYER_STATE_UNKNOWN self.supported_media_commands = 0 self.volume_level = 1 self.volume_muted = False self.media_custom_data = {} self.media_metadata = {} self.subtitle_tracks = {} self.current_subtitle_tracks = [] @property def metadata_type(self): """ Type of meta data. """ return self.media_metadata.get('metadataType') @property def player_is_playing(self): """ Return True if player is PLAYING. """ return (self.player_state == MEDIA_PLAYER_STATE_PLAYING or self.player_state == MEDIA_PLAYER_STATE_BUFFERING) @property def player_is_paused(self): """ Return True if player is PAUSED. """ return self.player_state == MEDIA_PLAYER_STATE_PAUSED @property def player_is_idle(self): """ Return True if player is IDLE. """ return self.player_state == MEDIA_PLAYER_STATE_IDLE @property def media_is_generic(self): """ Return True if media status represents generic media. """ return self.metadata_type == METADATA_TYPE_GENERIC @property def media_is_tvshow(self): """ Return True if media status represents a tv show. """ return self.metadata_type == METADATA_TYPE_TVSHOW @property def media_is_movie(self): """ Return True if media status represents a movie. """ return self.metadata_type == METADATA_TYPE_MOVIE @property def media_is_musictrack(self): """ Return True if media status represents a musictrack. """ return self.metadata_type == METADATA_TYPE_MUSICTRACK @property def media_is_photo(self): """ Return True if media status represents a photo. """ return self.metadata_type == METADATA_TYPE_PHOTO @property def stream_type_is_buffered(self): """ Return True if stream type is BUFFERED. """ return self.stream_type == STREAM_TYPE_BUFFERED @property def stream_type_is_live(self): """ Return True if stream type is LIVE. """ return self.stream_type == STREAM_TYPE_LIVE @property def title(self): """ Return title of media. """ return self.media_metadata.get('title') @property def series_title(self): """ Return series title if available. """ return self.media_metadata.get('seriesTitle') @property def season(self): """ Return season if available. """ return self.media_metadata.get('season') @property def episode(self): """ Return episode if available. """ return self.media_metadata.get('episode') @property def artist(self): """ Return artist if available. """ return self.media_metadata.get('artist') @property def album_name(self): """ Return album name if available. """ return self.media_metadata.get('albumName') @property def album_artist(self): """ Return album artist if available. """ return self.media_metadata.get('albumArtist') @property def track(self): """ Return track number if available. """ return self.media_metadata.get('track') @property def images(self): """ Return a list of MediaImage objects for this media. """ return [ MediaImage(item.get('url'), item.get('height'), item.get('width')) for item in self.media_metadata.get('images', []) ] @property def supports_pause(self): """ True if PAUSE is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_PAUSE) @property def supports_seek(self): """ True if SEEK is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_SEEK) @property def supports_stream_volume(self): """ True if STREAM_VOLUME is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_VOLUME) @property def supports_stream_mute(self): """ True if STREAM_MUTE is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_MUTE) @property def supports_skip_forward(self): """ True if SKIP_FORWARD is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_FORWARD) @property def supports_skip_backward(self): """ True if SKIP_BACKWARD is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_BACKWARD) def update(self, data): """ New data will only contain the changed attributes. """ if len(data.get('status', [])) == 0: return status_data = data['status'][0] media_data = status_data.get('media') or {} volume_data = status_data.get('volume', {}) self.current_time = status_data.get('currentTime', self.current_time) self.content_id = media_data.get('contentId', self.content_id) self.content_type = media_data.get('contentType', self.content_type) self.duration = media_data.get('duration', self.duration) self.stream_type = media_data.get('streamType', self.stream_type) self.idle_reason = status_data.get('idleReason', self.idle_reason) self.media_session_id = status_data.get( 'mediaSessionId', self.media_session_id) self.playback_rate = status_data.get( 'playbackRate', self.playback_rate) self.player_state = status_data.get('playerState', self.player_state) self.supported_media_commands = status_data.get( 'supportedMediaCommands', self.supported_media_commands) self.volume_level = volume_data.get('level', self.volume_level) self.volume_muted = volume_data.get('muted', self.volume_muted) self.media_custom_data = media_data.get( 'customData', self.media_custom_data) self.media_metadata = media_data.get('metadata', self.media_metadata) self.subtitle_tracks = media_data.get('tracks', self.subtitle_tracks) self.current_subtitle_tracks = status_data.get( 'activeTrackIds', self.current_subtitle_tracks) def __repr__(self): info = { 'metadata_type': self.metadata_type, 'title': self.title, 'series_title': self.series_title, 'season': self.season, 'episode': self.episode, 'artist': self.artist, 'album_name': self.album_name, 'album_artist': self.album_artist, 'track': self.track, 'subtitle_tracks': self.subtitle_tracks, 'images': self.images, 'supports_pause': self.supports_pause, 'supports_seek': self.supports_seek, 'supports_stream_volume': self.supports_stream_volume, 'supports_stream_mute': self.supports_stream_mute, 'supports_skip_forward': self.supports_skip_forward, 'supports_skip_backward': self.supports_skip_backward, } info.update(self.__dict__) return ''.format(info) # pylint: disable=too-many-public-methods class MediaController(BaseController): """ Controller to interact with Google media namespace. """ def __init__(self): super(MediaController, self).__init__( "urn:x-cast:com.google.cast.media") self.media_session_id = 0 self.status = MediaStatus() self.session_active_event = threading.Event() self.app_id = APP_MEDIA_RECEIVER self._status_listeners = [] def channel_connected(self): """ Called when media channel is connected. Will update status. """ self.update_status() def channel_disconnected(self): """ Called when a media channel is disconnected. Will erase status. """ self.status = MediaStatus() self._fire_status_changed() def receive_message(self, message, data): """ Called when a media message is received. """ if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS: self._process_media_status(data) return True else: return False def register_status_listener(self, listener): """ Register a listener for new media statusses. A new status will call listener.new_media_status(status) """ self._status_listeners.append(listener) def update_status(self, callback_function_param=False): """ Send message to update the status. """ self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS}, callback_function=callback_function_param) def _send_command(self, command): """ Send a command to the Chromecast on media channel. """ if self.status is None or self.status.media_session_id is None: self.logger.warning( "%s command requested but no session is active.", command[MESSAGE_TYPE]) return command['mediaSessionId'] = self.status.media_session_id self.send_message(command, inc_session_id=True) @property def is_playing(self): """ Deprecated as of June 8, 2015. Use self.status.player_is_playing. Returns if the Chromecast is playing. """ return self.status is not None and self.status.player_is_playing @property def is_paused(self): """ Deprecated as of June 8, 2015. Use self.status.player_is_paused. Returns if the Chromecast is paused. """ return self.status is not None and self.status.player_is_paused @property def is_idle(self): """ Deprecated as of June 8, 2015. Use self.status.player_is_idle. Returns if the Chromecast is idle on a media supported app. """ return self.status is not None and self.status.player_is_idle @property def title(self): """ Deprecated as of June 8, 2015. Use self.status.title. Return title of the current playing item. """ return None if not self.status else self.status.title @property def thumbnail(self): """ Deprecated as of June 8, 2015. Use self.status.images. Return thumbnail url of current playing item. """ if not self.status: return None images = self.status.images return images[0].url if images and len(images) > 0 else None def play(self): """ Send the PLAY command. """ self._send_command({MESSAGE_TYPE: TYPE_PLAY}) def pause(self): """ Send the PAUSE command. """ self._send_command({MESSAGE_TYPE: TYPE_PAUSE}) def stop(self): """ Send the STOP command. """ self._send_command({MESSAGE_TYPE: TYPE_STOP}) def rewind(self): """ Starts playing the media from the beginning. """ self.seek(0) def skip(self): """ Skips rest of the media. Values less then -5 behaved flaky. """ self.seek(int(self.status.duration)-5) def seek(self, position): """ Seek the media to a specific location. """ self._send_command({MESSAGE_TYPE: TYPE_SEEK, "currentTime": position, "resumeState": "PLAYBACK_START"}) def enable_subtitle(self, track_id): """ Enable specific text track. """ self._send_command({ MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO, "activeTrackIds": [track_id] }) def disable_subtitle(self): """ Disable subtitle. """ self._send_command({ MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO, "activeTrackIds": [] }) def block_until_active(self, timeout=None): """ Blocks thread until the media controller session is active on the chromecast. The media controller only accepts playback control commands when a media session is active. If a session is already active then the method returns immediately. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. """ self.session_active_event.wait(timeout=timeout) def _process_media_status(self, data): """ Processes a STATUS message. """ self.status.update(data) self.logger.debug("Media:Received status %s", data) # Update session active threading event if self.status.media_session_id is None: self.session_active_event.clear() else: self.session_active_event.set() self._fire_status_changed() def _fire_status_changed(self): """ Tells listeners of a changed status. """ for listener in self._status_listeners: try: listener.new_media_status(self.status) except Exception: # pylint: disable=broad-except pass # pylint: disable=too-many-arguments def play_media(self, url, content_type, title=None, thumb=None, current_time=0, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, metadata=None, subtitles=None, subtitles_lang='en-US', subtitles_mime='text/vtt', subtitle_id=1): """ Plays media on the Chromecast. Start default media receiver if not already started. Parameters: url: str - url of the media. content_type: str - mime type. Example: 'video/mp4'. title: str - title of the media. thumb: str - thumbnail image url. current_time: float - seconds from the beginning of the media to start playback. autoplay: bool - whether the media will automatically play. stream_type: str - describes the type of media artifact as one of the following: "NONE", "BUFFERED", "LIVE". subtitles: str - url of subtitle file to be shown on chromecast. subtitles_lang: str - language for subtitles. subtitles_mime: str - mimetype of subtitles. subtitle_id: int - id of subtitle to be loaded. metadata: dict - media metadata object, one of the following: GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata, MusicTrackMediaMetadata, PhotoMediaMetadata. Docs: https://developers.google.com/cast/docs/reference/messages#MediaData """ def app_launched_callback(): """Plays media after chromecast has switched to requested app.""" self._send_start_play_media( url, content_type, title, thumb, current_time, autoplay, stream_type, metadata, subtitles, subtitles_lang, subtitles_mime, subtitle_id) receiver_ctrl = self._socket_client.receiver_controller receiver_ctrl.launch_app(self.app_id, callback_function=app_launched_callback) def _send_start_play_media(self, url, content_type, title=None, thumb=None, current_time=0, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, metadata=None, subtitles=None, subtitles_lang='en-US', subtitles_mime='text/vtt', subtitle_id=1): msg = { 'media': { 'contentId': url, 'streamType': stream_type, 'contentType': content_type, 'metadata': metadata or {} }, MESSAGE_TYPE: TYPE_LOAD, 'currentTime': current_time, 'autoplay': autoplay, 'customData': {} } if title: msg['media']['metadata']['title'] = title if thumb: msg['media']['metadata']['thumb'] = thumb if 'images' not in msg['media']['metadata']: msg['media']['metadata']['images'] = [] msg['media']['metadata']['images'].append({'url': thumb}) if subtitles: sub_msg = [{ 'trackId': subtitle_id, 'trackContentId': subtitles, 'language': subtitles_lang, 'subtype': 'SUBTITLES', 'type': 'TEXT', 'trackContentType': subtitles_mime, 'name': "{} - {} Subtitle".format(subtitles_lang, subtitle_id) }] msg['media']['tracks'] = sub_msg msg['media']['textTrackStyle'] = { 'backgroundColor': '#FFFFFF00', 'edgeType': 'OUTLINE', 'edgeColor': '#000000FF' } msg['activeTrackIds'] = [subtitle_id] self.send_message(msg, inc_session_id=True) def tear_down(self): """ Called when controller is destroyed. """ super(MediaController, self).tear_down() self._status_listeners[:] = [] pychromecast-0.8.1/pychromecast/controllers/plex.py000066400000000000000000000012771306355002200226150ustar00rootroot00000000000000""" Controller to interface with the Plex-app. """ from . import BaseController MESSAGE_TYPE = 'type' TYPE_PLAY = "PLAY" TYPE_PAUSE = "PAUSE" TYPE_STOP = "STOP" class PlexController(BaseController): """ Controller to interact with Plex namespace. """ def __init__(self): super(PlexController, self).__init__( "urn:x-cast:plex", "9AC194DC") def stop(self): """ Send stop command. """ self.send_message({MESSAGE_TYPE: TYPE_STOP}) def pause(self): """ Send pause command. """ self.send_message({MESSAGE_TYPE: TYPE_PAUSE}) def play(self): """ Send play command. """ self.send_message({MESSAGE_TYPE: TYPE_PLAY}) pychromecast-0.8.1/pychromecast/controllers/youtube.py000066400000000000000000000030031306355002200233260ustar00rootroot00000000000000""" Controller to interface with the YouTube-app. Use the media controller to play, pause etc. """ from . import BaseController MESSAGE_TYPE = "type" TYPE_STATUS = "mdxSessionStatus" ATTR_SCREEN_ID = "screenId" class YouTubeController(BaseController): """ Controller to interact with Youtube namespace. """ def __init__(self): super(YouTubeController, self).__init__( "urn:x-cast:com.google.youtube.mdx", "233637DE") self.screen_id = None def receive_message(self, message, data): """ Called when a media message is received. """ if data[MESSAGE_TYPE] == TYPE_STATUS: self._process_status(data.get('data')) return True else: return False def play_video(self, youtube_id): """ Starts playing a video in the YouTube app. Only works if there is no video playing. """ def callback(): """Plays requested video after app launched.""" self.start_play(youtube_id) self.launch(callback) def start_play(self, youtube_id): """ Sends the play message to the YouTube app. """ msg = { "type": "flingVideo", "data": { "currentTime": 0, "videoId": youtube_id } } self.send_message(msg, inc_session_id=True) def _process_status(self, status): """ Process latest status update. """ self.screen_id = status.get(ATTR_SCREEN_ID) pychromecast-0.8.1/pychromecast/dial.py000066400000000000000000000074451306355002200202130ustar00rootroot00000000000000""" Implements the DIAL-protocol to communicate with the Chromecast """ import xml.etree.ElementTree as ET from collections import namedtuple from uuid import UUID import requests import six XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}" FORMAT_BASE_URL = "http://{}:8008" CC_SESSION = requests.Session() CC_SESSION.headers['content-type'] = 'application/json' # Regular chromecast, supports video/audio CAST_TYPE_CHROMECAST = 'cast' # Cast Audio device, supports only audio CAST_TYPE_AUDIO = 'audio' # Cast Audio group device, supports only audio CAST_TYPE_GROUP = 'group' CAST_TYPES = { u'chromecast': CAST_TYPE_CHROMECAST, u'chromecast audio': CAST_TYPE_AUDIO, u'google cast group': CAST_TYPE_GROUP, } def reboot(host): """ Reboots the chromecast. """ CC_SESSION.post(FORMAT_BASE_URL.format(host) + "/setup/reboot", data='{"params":"now"}', timeout=10) def get_device_status(host): """ :param host: Hostname or ip to fetch status from :type host: str :return: The device status as a named tuple. :rtype: pychromecast.dial.DeviceStatus or None """ try: req = CC_SESSION.get( FORMAT_BASE_URL.format(host) + "/ssdp/device-desc.xml", timeout=10) # The Requests library will fall back to guessing the encoding in case # no encoding is specified in the response headers - which is the case # for the Chromecast. # The standard mandates utf-8 encoding, let's fall back to that instead # if no encoding is provided, since the autodetection does not always # provide correct results. if req.encoding is None: req.encoding = 'utf-8' status_el = ET.fromstring(req.text.encode("UTF-8")) device_info_el = status_el.find(XML_NS_UPNP_DEVICE + "device") api_version_el = status_el.find(XML_NS_UPNP_DEVICE + "specVersion") friendly_name = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE, "friendlyName", "Unknown Chromecast") model_name = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE, "modelName", "Unknown model name") manufacturer = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE, "manufacturer", "Unknown manufacturer") udn = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE, "UDN", None) api_version = (int(_read_xml_element(api_version_el, XML_NS_UPNP_DEVICE, "major", -1)), int(_read_xml_element(api_version_el, XML_NS_UPNP_DEVICE, "minor", -1))) cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST) uuid = None if udn and udn.startswith('uuid:'): uuid = UUID(udn[len('uuid:'):].replace("-", "")) return DeviceStatus(friendly_name, model_name, manufacturer, api_version, uuid, cast_type) except (requests.exceptions.RequestException, ET.ParseError): return None def _read_xml_element(element, xml_ns, tag_name, default=""): """ Helper method to read text from an element. """ try: text = element.find(xml_ns + tag_name).text if isinstance(text, six.text_type): return text else: return text.decode('utf-8') except AttributeError: return default DeviceStatus = namedtuple("DeviceStatus", ["friendly_name", "model_name", "manufacturer", "api_version", "uuid", "cast_type"]) pychromecast-0.8.1/pychromecast/discovery.py000066400000000000000000000070141306355002200213010ustar00rootroot00000000000000"""Discovers Chromecasts on the network using mDNS/zeroconf.""" from uuid import UUID import six from zeroconf import ServiceBrowser, Zeroconf DISCOVER_TIMEOUT = 5 class CastListener(object): """Zeroconf Cast Services collection.""" def __init__(self, callback=None): self.services = {} self.callback = callback @property def count(self): """Number of discovered cast services.""" return len(self.services) @property def devices(self): """List of tuples (ip, host) for each discovered device.""" return list(self.services.values()) # pylint: disable=unused-argument def remove_service(self, zconf, typ, name): """ Remove a service from the collection. """ self.services.pop(name, None) def add_service(self, zconf, typ, name): """ Add a service to the collection. """ service = None tries = 0 while service is None and tries < 4: try: service = zconf.get_service_info(typ, name) except IOError: # If the zerconf fails to receive the necesarry data we abort # adding the service break tries += 1 if not service: return def get_value(key): """Retrieve value and decode for Python 2/3.""" value = service.properties.get(key.encode('utf-8')) if value is None or isinstance(value, six.text_type): return value return value.decode('utf-8') ips = zconf.cache.entries_with_name(service.server.lower()) host = repr(ips[0]) if ips else service.server model_name = get_value('md') uuid = get_value('id') friendly_name = get_value('fn') if uuid: uuid = UUID(uuid) self.services[name] = (host, service.port, uuid, model_name, friendly_name) if self.callback: self.callback(name) def start_discovery(callback=None): """ Start discovering chromecasts on the network. This method will start discovering chromecasts on a separate thread. When a chromecast is discovered, the callback will be called with the discovered chromecast's zeroconf name. This is the dictionary key to find the chromecast metadata in listener.services. This method returns the CastListener object and the zeroconf ServiceBrowser object. The CastListener object will contain information for the discovered chromecasts. To stop discovery, call the stop_discovery method with the ServiceBrowser object. """ listener = CastListener(callback) return listener, \ ServiceBrowser(Zeroconf(), "_googlecast._tcp.local.", listener) def stop_discovery(browser): """Stop the chromecast discovery thread.""" browser.zc.close() def discover_chromecasts(max_devices=None, timeout=DISCOVER_TIMEOUT): """ Discover chromecasts on the network. """ from threading import Event try: # pylint: disable=unused-argument def callback(name): """Called when zeroconf has discovered a new chromecast.""" if max_devices is not None and listener.count >= max_devices: discover_complete.set() discover_complete = Event() listener, browser = start_discovery(callback) # Wait for the timeout or the maximum number of devices discover_complete.wait(timeout) return listener.devices finally: stop_discovery(browser) pychromecast-0.8.1/pychromecast/error.py000066400000000000000000000024661306355002200204310ustar00rootroot00000000000000""" Errors to be used by PyChromecast. """ class PyChromecastError(Exception): """ Base error for PyChromecast. """ pass class NoChromecastFoundError(PyChromecastError): """ When a command has to auto-discover a Chromecast and cannot find one. """ pass class MultipleChromecastsFoundError(PyChromecastError): """ When getting a singular chromecast results in getting multiple chromecasts. """ pass class ChromecastConnectionError(PyChromecastError): """ When a connection error occurs within PyChromecast. """ pass class LaunchError(PyChromecastError): """ When an app fails to launch. """ pass class PyChromecastStopped(PyChromecastError): """ Raised when a command is invoked while the Chromecast's socket_client is stopped. """ pass class NotConnected(PyChromecastError): """ Raised when a command is invoked while not connected to a Chromecast. """ pass class UnsupportedNamespace(PyChromecastError): """ Raised when trying to send a message with a namespace that is not supported by the current running app. """ pass class ControllerNotRegistered(PyChromecastError): """ Raised when trying to interact with a controller while it is not registered with a ChromeCast object. """ pass pychromecast-0.8.1/pychromecast/logging_pb2.py000066400000000000000000001072371306355002200214730ustar00rootroot00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: logging.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='logging.proto', package='extensions.api.cast_channel.proto', syntax='proto2', serialized_pb=_b('\n\rlogging.proto\x12!extensions.api.cast_channel.proto\"\xfd\x04\n\x0bSocketEvent\x12:\n\x04type\x18\x01 \x01(\x0e\x32,.extensions.api.cast_channel.proto.EventType\x12\x18\n\x10timestamp_micros\x18\x02 \x01(\x03\x12\x0f\n\x07\x64\x65tails\x18\x03 \x01(\t\x12\x18\n\x10net_return_value\x18\x04 \x01(\x05\x12\x19\n\x11message_namespace\x18\x05 \x01(\t\x12\x42\n\x0bready_state\x18\x06 \x01(\x0e\x32-.extensions.api.cast_channel.proto.ReadyState\x12L\n\x10\x63onnection_state\x18\x07 \x01(\x0e\x32\x32.extensions.api.cast_channel.proto.ConnectionState\x12@\n\nread_state\x18\x08 \x01(\x0e\x32,.extensions.api.cast_channel.proto.ReadState\x12\x42\n\x0bwrite_state\x18\t \x01(\x0e\x32-.extensions.api.cast_channel.proto.WriteState\x12\x42\n\x0b\x65rror_state\x18\n \x01(\x0e\x32-.extensions.api.cast_channel.proto.ErrorState\x12^\n\x1a\x63hallenge_reply_error_type\x18\x0b \x01(\x0e\x32:.extensions.api.cast_channel.proto.ChallengeReplyErrorType\x12\x16\n\x0enss_error_code\x18\x0c \x01(\x05\"\xf4\x01\n\x15\x41ggregatedSocketEvent\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x13\n\x0b\x65ndpoint_id\x18\x02 \x01(\x05\x12I\n\x11\x63hannel_auth_type\x18\x03 \x01(\x0e\x32..extensions.api.cast_channel.proto.ChannelAuth\x12\x44\n\x0csocket_event\x18\x04 \x03(\x0b\x32..extensions.api.cast_channel.proto.SocketEvent\x12\x12\n\nbytes_read\x18\x05 \x01(\x03\x12\x15\n\rbytes_written\x18\x06 \x01(\x03\"\xb1\x01\n\x03Log\x12Y\n\x17\x61ggregated_socket_event\x18\x01 \x03(\x0b\x32\x38.extensions.api.cast_channel.proto.AggregatedSocketEvent\x12,\n$num_evicted_aggregated_socket_events\x18\x02 \x01(\x05\x12!\n\x19num_evicted_socket_events\x18\x03 \x01(\x05*\xc0\x06\n\tEventType\x12\x16\n\x12\x45VENT_TYPE_UNKNOWN\x10\x00\x12\x17\n\x13\x43\x41ST_SOCKET_CREATED\x10\x01\x12\x17\n\x13READY_STATE_CHANGED\x10\x02\x12\x1c\n\x18\x43ONNECTION_STATE_CHANGED\x10\x03\x12\x16\n\x12READ_STATE_CHANGED\x10\x04\x12\x17\n\x13WRITE_STATE_CHANGED\x10\x05\x12\x17\n\x13\x45RROR_STATE_CHANGED\x10\x06\x12\x12\n\x0e\x43ONNECT_FAILED\x10\x07\x12\x16\n\x12TCP_SOCKET_CONNECT\x10\x08\x12\x1d\n\x19TCP_SOCKET_SET_KEEP_ALIVE\x10\t\x12\x18\n\x14SSL_CERT_WHITELISTED\x10\n\x12\x16\n\x12SSL_SOCKET_CONNECT\x10\x0b\x12\x15\n\x11SSL_INFO_OBTAINED\x10\x0c\x12\x1b\n\x17\x44\x45R_ENCODED_CERT_OBTAIN\x10\r\x12\x1c\n\x18RECEIVED_CHALLENGE_REPLY\x10\x0e\x12\x18\n\x14\x41UTH_CHALLENGE_REPLY\x10\x0f\x12\x15\n\x11\x43ONNECT_TIMED_OUT\x10\x10\x12\x17\n\x13SEND_MESSAGE_FAILED\x10\x11\x12\x14\n\x10MESSAGE_ENQUEUED\x10\x12\x12\x10\n\x0cSOCKET_WRITE\x10\x13\x12\x13\n\x0fMESSAGE_WRITTEN\x10\x14\x12\x0f\n\x0bSOCKET_READ\x10\x15\x12\x10\n\x0cMESSAGE_READ\x10\x16\x12\x11\n\rSOCKET_CLOSED\x10\x19\x12\x1f\n\x1bSSL_CERT_EXCESSIVE_LIFETIME\x10\x1a\x12\x1b\n\x17\x43HANNEL_POLICY_ENFORCED\x10\x1b\x12\x1f\n\x1bTCP_SOCKET_CONNECT_COMPLETE\x10\x1c\x12\x1f\n\x1bSSL_SOCKET_CONNECT_COMPLETE\x10\x1d\x12\x1d\n\x19SSL_SOCKET_CONNECT_FAILED\x10\x1e\x12\x1e\n\x1aSEND_AUTH_CHALLENGE_FAILED\x10\x1f\x12 \n\x1c\x41UTH_CHALLENGE_REPLY_INVALID\x10 \x12\x14\n\x10PING_WRITE_ERROR\x10!*(\n\x0b\x43hannelAuth\x12\x07\n\x03SSL\x10\x01\x12\x10\n\x0cSSL_VERIFIED\x10\x02*\x85\x01\n\nReadyState\x12\x14\n\x10READY_STATE_NONE\x10\x01\x12\x1a\n\x16READY_STATE_CONNECTING\x10\x02\x12\x14\n\x10READY_STATE_OPEN\x10\x03\x12\x17\n\x13READY_STATE_CLOSING\x10\x04\x12\x16\n\x12READY_STATE_CLOSED\x10\x05*\x8f\x03\n\x0f\x43onnectionState\x12\x16\n\x12\x43ONN_STATE_UNKNOWN\x10\x01\x12\x1a\n\x16\x43ONN_STATE_TCP_CONNECT\x10\x02\x12#\n\x1f\x43ONN_STATE_TCP_CONNECT_COMPLETE\x10\x03\x12\x1a\n\x16\x43ONN_STATE_SSL_CONNECT\x10\x04\x12#\n\x1f\x43ONN_STATE_SSL_CONNECT_COMPLETE\x10\x05\x12\"\n\x1e\x43ONN_STATE_AUTH_CHALLENGE_SEND\x10\x06\x12+\n\'CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE\x10\x07\x12,\n(CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE\x10\x08\x12\x1c\n\x18\x43ONN_STATE_START_CONNECT\x10\t\x12\x17\n\x13\x43ONN_STATE_FINISHED\x10\x64\x12\x14\n\x10\x43ONN_STATE_ERROR\x10\x65\x12\x16\n\x12\x43ONN_STATE_TIMEOUT\x10\x66*\xa5\x01\n\tReadState\x12\x16\n\x12READ_STATE_UNKNOWN\x10\x01\x12\x13\n\x0fREAD_STATE_READ\x10\x02\x12\x1c\n\x18READ_STATE_READ_COMPLETE\x10\x03\x12\x1a\n\x16READ_STATE_DO_CALLBACK\x10\x04\x12\x1b\n\x17READ_STATE_HANDLE_ERROR\x10\x05\x12\x14\n\x10READ_STATE_ERROR\x10\x64*\xc4\x01\n\nWriteState\x12\x17\n\x13WRITE_STATE_UNKNOWN\x10\x01\x12\x15\n\x11WRITE_STATE_WRITE\x10\x02\x12\x1e\n\x1aWRITE_STATE_WRITE_COMPLETE\x10\x03\x12\x1b\n\x17WRITE_STATE_DO_CALLBACK\x10\x04\x12\x1c\n\x18WRITE_STATE_HANDLE_ERROR\x10\x05\x12\x15\n\x11WRITE_STATE_ERROR\x10\x64\x12\x14\n\x10WRITE_STATE_IDLE\x10\x65*\xdb\x02\n\nErrorState\x12\x16\n\x12\x43HANNEL_ERROR_NONE\x10\x01\x12\"\n\x1e\x43HANNEL_ERROR_CHANNEL_NOT_OPEN\x10\x02\x12&\n\"CHANNEL_ERROR_AUTHENTICATION_ERROR\x10\x03\x12\x1f\n\x1b\x43HANNEL_ERROR_CONNECT_ERROR\x10\x04\x12\x1e\n\x1a\x43HANNEL_ERROR_SOCKET_ERROR\x10\x05\x12!\n\x1d\x43HANNEL_ERROR_TRANSPORT_ERROR\x10\x06\x12!\n\x1d\x43HANNEL_ERROR_INVALID_MESSAGE\x10\x07\x12$\n CHANNEL_ERROR_INVALID_CHANNEL_ID\x10\x08\x12!\n\x1d\x43HANNEL_ERROR_CONNECT_TIMEOUT\x10\t\x12\x19\n\x15\x43HANNEL_ERROR_UNKNOWN\x10\n*\xb0\x06\n\x17\x43hallengeReplyErrorType\x12\x1e\n\x1a\x43HALLENGE_REPLY_ERROR_NONE\x10\x01\x12)\n%CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY\x10\x02\x12,\n(CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE\x10\x03\x12$\n CHALLENGE_REPLY_ERROR_NO_PAYLOAD\x10\x04\x12\x30\n,CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED\x10\x05\x12\'\n#CHALLENGE_REPLY_ERROR_MESSAGE_ERROR\x10\x06\x12%\n!CHALLENGE_REPLY_ERROR_NO_RESPONSE\x10\x07\x12/\n+CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND\x10\x08\x12-\n)CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED\x10\t\x12\x37\n3CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA\x10\n\x12\x33\n/CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY\x10\x0b\x12/\n+CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH\x10\x0c\x12;\n7CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG\x10\r\x12=\n9CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE\x10\x0e\x12*\n&CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED\x10\x0f\x12%\n!CHALLENGE_REPLY_ERROR_CRL_INVALID\x10\x10\x12&\n\"CHALLENGE_REPLY_ERROR_CERT_REVOKED\x10\x11\x42\x02H\x03') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _EVENTTYPE = _descriptor.EnumDescriptor( name='EventType', full_name='extensions.api.cast_channel.proto.EventType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='EVENT_TYPE_UNKNOWN', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='CAST_SOCKET_CREATED', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CHANGED', index=2, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONNECTION_STATE_CHANGED', index=3, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_CHANGED', index=4, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_CHANGED', index=5, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='ERROR_STATE_CHANGED', index=6, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONNECT_FAILED', index=7, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='TCP_SOCKET_CONNECT', index=8, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='TCP_SOCKET_SET_KEEP_ALIVE', index=9, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_CERT_WHITELISTED', index=10, number=10, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_SOCKET_CONNECT', index=11, number=11, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_INFO_OBTAINED', index=12, number=12, options=None, type=None), _descriptor.EnumValueDescriptor( name='DER_ENCODED_CERT_OBTAIN', index=13, number=13, options=None, type=None), _descriptor.EnumValueDescriptor( name='RECEIVED_CHALLENGE_REPLY', index=14, number=14, options=None, type=None), _descriptor.EnumValueDescriptor( name='AUTH_CHALLENGE_REPLY', index=15, number=15, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONNECT_TIMED_OUT', index=16, number=16, options=None, type=None), _descriptor.EnumValueDescriptor( name='SEND_MESSAGE_FAILED', index=17, number=17, options=None, type=None), _descriptor.EnumValueDescriptor( name='MESSAGE_ENQUEUED', index=18, number=18, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOCKET_WRITE', index=19, number=19, options=None, type=None), _descriptor.EnumValueDescriptor( name='MESSAGE_WRITTEN', index=20, number=20, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOCKET_READ', index=21, number=21, options=None, type=None), _descriptor.EnumValueDescriptor( name='MESSAGE_READ', index=22, number=22, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOCKET_CLOSED', index=23, number=25, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_CERT_EXCESSIVE_LIFETIME', index=24, number=26, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_POLICY_ENFORCED', index=25, number=27, options=None, type=None), _descriptor.EnumValueDescriptor( name='TCP_SOCKET_CONNECT_COMPLETE', index=26, number=28, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_SOCKET_CONNECT_COMPLETE', index=27, number=29, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_SOCKET_CONNECT_FAILED', index=28, number=30, options=None, type=None), _descriptor.EnumValueDescriptor( name='SEND_AUTH_CHALLENGE_FAILED', index=29, number=31, options=None, type=None), _descriptor.EnumValueDescriptor( name='AUTH_CHALLENGE_REPLY_INVALID', index=30, number=32, options=None, type=None), _descriptor.EnumValueDescriptor( name='PING_WRITE_ERROR', index=31, number=33, options=None, type=None), ], containing_type=None, options=None, serialized_start=1120, serialized_end=1952, ) _sym_db.RegisterEnumDescriptor(_EVENTTYPE) EventType = enum_type_wrapper.EnumTypeWrapper(_EVENTTYPE) _CHANNELAUTH = _descriptor.EnumDescriptor( name='ChannelAuth', full_name='extensions.api.cast_channel.proto.ChannelAuth', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SSL', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_VERIFIED', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1954, serialized_end=1994, ) _sym_db.RegisterEnumDescriptor(_CHANNELAUTH) ChannelAuth = enum_type_wrapper.EnumTypeWrapper(_CHANNELAUTH) _READYSTATE = _descriptor.EnumDescriptor( name='ReadyState', full_name='extensions.api.cast_channel.proto.ReadyState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='READY_STATE_NONE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CONNECTING', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_OPEN', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CLOSING', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CLOSED', index=4, number=5, options=None, type=None), ], containing_type=None, options=None, serialized_start=1997, serialized_end=2130, ) _sym_db.RegisterEnumDescriptor(_READYSTATE) ReadyState = enum_type_wrapper.EnumTypeWrapper(_READYSTATE) _CONNECTIONSTATE = _descriptor.EnumDescriptor( name='ConnectionState', full_name='extensions.api.cast_channel.proto.ConnectionState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CONN_STATE_UNKNOWN', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_TCP_CONNECT', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_TCP_CONNECT_COMPLETE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_SSL_CONNECT', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_SSL_CONNECT_COMPLETE', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_AUTH_CHALLENGE_SEND', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_START_CONNECT', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_FINISHED', index=9, number=100, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_ERROR', index=10, number=101, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_TIMEOUT', index=11, number=102, options=None, type=None), ], containing_type=None, options=None, serialized_start=2133, serialized_end=2532, ) _sym_db.RegisterEnumDescriptor(_CONNECTIONSTATE) ConnectionState = enum_type_wrapper.EnumTypeWrapper(_CONNECTIONSTATE) _READSTATE = _descriptor.EnumDescriptor( name='ReadState', full_name='extensions.api.cast_channel.proto.ReadState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='READ_STATE_UNKNOWN', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_READ', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_READ_COMPLETE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_DO_CALLBACK', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_HANDLE_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_ERROR', index=5, number=100, options=None, type=None), ], containing_type=None, options=None, serialized_start=2535, serialized_end=2700, ) _sym_db.RegisterEnumDescriptor(_READSTATE) ReadState = enum_type_wrapper.EnumTypeWrapper(_READSTATE) _WRITESTATE = _descriptor.EnumDescriptor( name='WriteState', full_name='extensions.api.cast_channel.proto.WriteState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='WRITE_STATE_UNKNOWN', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_WRITE', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_WRITE_COMPLETE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_DO_CALLBACK', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_HANDLE_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_ERROR', index=5, number=100, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_IDLE', index=6, number=101, options=None, type=None), ], containing_type=None, options=None, serialized_start=2703, serialized_end=2899, ) _sym_db.RegisterEnumDescriptor(_WRITESTATE) WriteState = enum_type_wrapper.EnumTypeWrapper(_WRITESTATE) _ERRORSTATE = _descriptor.EnumDescriptor( name='ErrorState', full_name='extensions.api.cast_channel.proto.ErrorState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_NONE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_CHANNEL_NOT_OPEN', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_AUTHENTICATION_ERROR', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_CONNECT_ERROR', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_SOCKET_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_TRANSPORT_ERROR', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_INVALID_MESSAGE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_INVALID_CHANNEL_ID', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_CONNECT_TIMEOUT', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_UNKNOWN', index=9, number=10, options=None, type=None), ], containing_type=None, options=None, serialized_start=2902, serialized_end=3249, ) _sym_db.RegisterEnumDescriptor(_ERRORSTATE) ErrorState = enum_type_wrapper.EnumTypeWrapper(_ERRORSTATE) _CHALLENGEREPLYERRORTYPE = _descriptor.EnumDescriptor( name='ChallengeReplyErrorType', full_name='extensions.api.cast_channel.proto.ChallengeReplyErrorType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_NONE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_NO_PAYLOAD', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_MESSAGE_ERROR', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_NO_RESPONSE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA', index=9, number=10, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY', index=10, number=11, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH', index=11, number=12, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG', index=12, number=13, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE', index=13, number=14, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED', index=14, number=15, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CRL_INVALID', index=15, number=16, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CERT_REVOKED', index=16, number=17, options=None, type=None), ], containing_type=None, options=None, serialized_start=3252, serialized_end=4068, ) _sym_db.RegisterEnumDescriptor(_CHALLENGEREPLYERRORTYPE) ChallengeReplyErrorType = enum_type_wrapper.EnumTypeWrapper(_CHALLENGEREPLYERRORTYPE) EVENT_TYPE_UNKNOWN = 0 CAST_SOCKET_CREATED = 1 READY_STATE_CHANGED = 2 CONNECTION_STATE_CHANGED = 3 READ_STATE_CHANGED = 4 WRITE_STATE_CHANGED = 5 ERROR_STATE_CHANGED = 6 CONNECT_FAILED = 7 TCP_SOCKET_CONNECT = 8 TCP_SOCKET_SET_KEEP_ALIVE = 9 SSL_CERT_WHITELISTED = 10 SSL_SOCKET_CONNECT = 11 SSL_INFO_OBTAINED = 12 DER_ENCODED_CERT_OBTAIN = 13 RECEIVED_CHALLENGE_REPLY = 14 AUTH_CHALLENGE_REPLY = 15 CONNECT_TIMED_OUT = 16 SEND_MESSAGE_FAILED = 17 MESSAGE_ENQUEUED = 18 SOCKET_WRITE = 19 MESSAGE_WRITTEN = 20 SOCKET_READ = 21 MESSAGE_READ = 22 SOCKET_CLOSED = 25 SSL_CERT_EXCESSIVE_LIFETIME = 26 CHANNEL_POLICY_ENFORCED = 27 TCP_SOCKET_CONNECT_COMPLETE = 28 SSL_SOCKET_CONNECT_COMPLETE = 29 SSL_SOCKET_CONNECT_FAILED = 30 SEND_AUTH_CHALLENGE_FAILED = 31 AUTH_CHALLENGE_REPLY_INVALID = 32 PING_WRITE_ERROR = 33 SSL = 1 SSL_VERIFIED = 2 READY_STATE_NONE = 1 READY_STATE_CONNECTING = 2 READY_STATE_OPEN = 3 READY_STATE_CLOSING = 4 READY_STATE_CLOSED = 5 CONN_STATE_UNKNOWN = 1 CONN_STATE_TCP_CONNECT = 2 CONN_STATE_TCP_CONNECT_COMPLETE = 3 CONN_STATE_SSL_CONNECT = 4 CONN_STATE_SSL_CONNECT_COMPLETE = 5 CONN_STATE_AUTH_CHALLENGE_SEND = 6 CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE = 7 CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE = 8 CONN_STATE_START_CONNECT = 9 CONN_STATE_FINISHED = 100 CONN_STATE_ERROR = 101 CONN_STATE_TIMEOUT = 102 READ_STATE_UNKNOWN = 1 READ_STATE_READ = 2 READ_STATE_READ_COMPLETE = 3 READ_STATE_DO_CALLBACK = 4 READ_STATE_HANDLE_ERROR = 5 READ_STATE_ERROR = 100 WRITE_STATE_UNKNOWN = 1 WRITE_STATE_WRITE = 2 WRITE_STATE_WRITE_COMPLETE = 3 WRITE_STATE_DO_CALLBACK = 4 WRITE_STATE_HANDLE_ERROR = 5 WRITE_STATE_ERROR = 100 WRITE_STATE_IDLE = 101 CHANNEL_ERROR_NONE = 1 CHANNEL_ERROR_CHANNEL_NOT_OPEN = 2 CHANNEL_ERROR_AUTHENTICATION_ERROR = 3 CHANNEL_ERROR_CONNECT_ERROR = 4 CHANNEL_ERROR_SOCKET_ERROR = 5 CHANNEL_ERROR_TRANSPORT_ERROR = 6 CHANNEL_ERROR_INVALID_MESSAGE = 7 CHANNEL_ERROR_INVALID_CHANNEL_ID = 8 CHANNEL_ERROR_CONNECT_TIMEOUT = 9 CHANNEL_ERROR_UNKNOWN = 10 CHALLENGE_REPLY_ERROR_NONE = 1 CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY = 2 CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE = 3 CHALLENGE_REPLY_ERROR_NO_PAYLOAD = 4 CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED = 5 CHALLENGE_REPLY_ERROR_MESSAGE_ERROR = 6 CHALLENGE_REPLY_ERROR_NO_RESPONSE = 7 CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND = 8 CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED = 9 CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA = 10 CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY = 11 CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH = 12 CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG = 13 CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE = 14 CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED = 15 CHALLENGE_REPLY_ERROR_CRL_INVALID = 16 CHALLENGE_REPLY_ERROR_CERT_REVOKED = 17 _SOCKETEVENT = _descriptor.Descriptor( name='SocketEvent', full_name='extensions.api.cast_channel.proto.SocketEvent', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='extensions.api.cast_channel.proto.SocketEvent.type', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='timestamp_micros', full_name='extensions.api.cast_channel.proto.SocketEvent.timestamp_micros', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='details', full_name='extensions.api.cast_channel.proto.SocketEvent.details', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='net_return_value', full_name='extensions.api.cast_channel.proto.SocketEvent.net_return_value', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='message_namespace', full_name='extensions.api.cast_channel.proto.SocketEvent.message_namespace', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='ready_state', full_name='extensions.api.cast_channel.proto.SocketEvent.ready_state', index=5, number=6, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='connection_state', full_name='extensions.api.cast_channel.proto.SocketEvent.connection_state', index=6, number=7, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='read_state', full_name='extensions.api.cast_channel.proto.SocketEvent.read_state', index=7, number=8, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='write_state', full_name='extensions.api.cast_channel.proto.SocketEvent.write_state', index=8, number=9, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='error_state', full_name='extensions.api.cast_channel.proto.SocketEvent.error_state', index=9, number=10, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='challenge_reply_error_type', full_name='extensions.api.cast_channel.proto.SocketEvent.challenge_reply_error_type', index=10, number=11, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='nss_error_code', full_name='extensions.api.cast_channel.proto.SocketEvent.nss_error_code', index=11, number=12, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=53, serialized_end=690, ) _AGGREGATEDSOCKETEVENT = _descriptor.Descriptor( name='AggregatedSocketEvent', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.id', index=0, number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='endpoint_id', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.endpoint_id', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='channel_auth_type', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.channel_auth_type', index=2, number=3, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='socket_event', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.socket_event', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='bytes_read', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.bytes_read', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='bytes_written', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.bytes_written', index=5, number=6, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=693, serialized_end=937, ) _LOG = _descriptor.Descriptor( name='Log', full_name='extensions.api.cast_channel.proto.Log', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='aggregated_socket_event', full_name='extensions.api.cast_channel.proto.Log.aggregated_socket_event', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='num_evicted_aggregated_socket_events', full_name='extensions.api.cast_channel.proto.Log.num_evicted_aggregated_socket_events', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='num_evicted_socket_events', full_name='extensions.api.cast_channel.proto.Log.num_evicted_socket_events', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=940, serialized_end=1117, ) _SOCKETEVENT.fields_by_name['type'].enum_type = _EVENTTYPE _SOCKETEVENT.fields_by_name['ready_state'].enum_type = _READYSTATE _SOCKETEVENT.fields_by_name['connection_state'].enum_type = _CONNECTIONSTATE _SOCKETEVENT.fields_by_name['read_state'].enum_type = _READSTATE _SOCKETEVENT.fields_by_name['write_state'].enum_type = _WRITESTATE _SOCKETEVENT.fields_by_name['error_state'].enum_type = _ERRORSTATE _SOCKETEVENT.fields_by_name['challenge_reply_error_type'].enum_type = _CHALLENGEREPLYERRORTYPE _AGGREGATEDSOCKETEVENT.fields_by_name['channel_auth_type'].enum_type = _CHANNELAUTH _AGGREGATEDSOCKETEVENT.fields_by_name['socket_event'].message_type = _SOCKETEVENT _LOG.fields_by_name['aggregated_socket_event'].message_type = _AGGREGATEDSOCKETEVENT DESCRIPTOR.message_types_by_name['SocketEvent'] = _SOCKETEVENT DESCRIPTOR.message_types_by_name['AggregatedSocketEvent'] = _AGGREGATEDSOCKETEVENT DESCRIPTOR.message_types_by_name['Log'] = _LOG DESCRIPTOR.enum_types_by_name['EventType'] = _EVENTTYPE DESCRIPTOR.enum_types_by_name['ChannelAuth'] = _CHANNELAUTH DESCRIPTOR.enum_types_by_name['ReadyState'] = _READYSTATE DESCRIPTOR.enum_types_by_name['ConnectionState'] = _CONNECTIONSTATE DESCRIPTOR.enum_types_by_name['ReadState'] = _READSTATE DESCRIPTOR.enum_types_by_name['WriteState'] = _WRITESTATE DESCRIPTOR.enum_types_by_name['ErrorState'] = _ERRORSTATE DESCRIPTOR.enum_types_by_name['ChallengeReplyErrorType'] = _CHALLENGEREPLYERRORTYPE SocketEvent = _reflection.GeneratedProtocolMessageType('SocketEvent', (_message.Message,), dict( DESCRIPTOR = _SOCKETEVENT, __module__ = 'logging_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.SocketEvent) )) _sym_db.RegisterMessage(SocketEvent) AggregatedSocketEvent = _reflection.GeneratedProtocolMessageType('AggregatedSocketEvent', (_message.Message,), dict( DESCRIPTOR = _AGGREGATEDSOCKETEVENT, __module__ = 'logging_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AggregatedSocketEvent) )) _sym_db.RegisterMessage(AggregatedSocketEvent) Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), dict( DESCRIPTOR = _LOG, __module__ = 'logging_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.Log) )) _sym_db.RegisterMessage(Log) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003')) # @@protoc_insertion_point(module_scope) pychromecast-0.8.1/pychromecast/socket_client.py000066400000000000000000001024341306355002200221220ustar00rootroot00000000000000""" Module to interact with the ChromeCast via protobuf-over-socket. Big thanks goes out to Fred Clift who build the first version of this code: https://github.com/minektur/chromecast-python-poc. Without him this would not have been possible. """ # Pylint does not understand the protobuf objects correctly # pylint: disable=no-member import logging import select import socket import ssl import json import threading import time from collections import namedtuple from struct import pack, unpack import sys from . import cast_channel_pb2 from .dial import CAST_TYPE_CHROMECAST, CAST_TYPE_AUDIO, CAST_TYPE_GROUP from .controllers import BaseController from .controllers.media import MediaController from .error import ( ChromecastConnectionError, UnsupportedNamespace, NotConnected, PyChromecastStopped, LaunchError, ) NS_CONNECTION = 'urn:x-cast:com.google.cast.tp.connection' NS_RECEIVER = 'urn:x-cast:com.google.cast.receiver' NS_HEARTBEAT = 'urn:x-cast:com.google.cast.tp.heartbeat' PLATFORM_DESTINATION_ID = "receiver-0" MESSAGE_TYPE = 'type' TYPE_PING = "PING" TYPE_RECEIVER_STATUS = "RECEIVER_STATUS" TYPE_PONG = "PONG" TYPE_CONNECT = "CONNECT" TYPE_CLOSE = "CLOSE" TYPE_GET_STATUS = "GET_STATUS" TYPE_LAUNCH = "LAUNCH" TYPE_LAUNCH_ERROR = "LAUNCH_ERROR" TYPE_LOAD = "LOAD" # The socket connection is being setup CONNECTION_STATUS_CONNECTING = "CONNECTING" # The socket connection was complete CONNECTION_STATUS_CONNECTED = "CONNECTED" # The socket connection has been disconnected CONNECTION_STATUS_DISCONNECTED = "DISCONNECTED" # Connecting to socket failed (after a CONNECTION_STATUS_CONNECTING) CONNECTION_STATUS_FAILED = "FAILED" # The socket connection was lost and needs to be retried CONNECTION_STATUS_LOST = "LOST" APP_ID = 'appId' REQUEST_ID = "requestId" SESSION_ID = "sessionId" ERROR_REASON = 'reason' HB_PING_TIME = 10 HB_PONG_TIME = 10 POLL_TIME_BLOCKING = 5.0 POLL_TIME_NON_BLOCKING = 0.01 TIMEOUT_TIME = 30 RETRY_TIME = 5 class InterruptLoop(Exception): """ The chromecast has been manually stopped. """ pass def _json_from_message(message): """ Parses a PB2 message into JSON format. """ try: return json.loads(message.payload_utf8) except ValueError: logger = logging.getLogger(__name__) logger.warning("Ignoring invalid json in namespace %s: %s", message.namespace, message.payload_utf8) return {} def _message_to_string(message, data=None): """ Gives a string representation of a PB2 message. """ if data is None: data = _json_from_message(message) return "Message {} from {} to {}: {}".format( message.namespace, message.source_id, message.destination_id, data) if sys.version_info >= (3, 0): def _json_to_payload(data): """ Encodes a python value into JSON format. """ return json.dumps(data, ensure_ascii=False).encode("utf8") else: def _json_to_payload(data): """ Encodes a python value into JSON format. """ return json.dumps(data, ensure_ascii=False) def _is_ssl_timeout(exc): """ Returns True if the exception is for an SSL timeout """ return exc.message in ("The handshake operation timed out", "The write operation timed out", "The read operation timed out") NetworkAddress = namedtuple('NetworkAddress', ['address', 'port']) ConnectionStatus = namedtuple('ConnectionStatus', ['status', 'address']) CastStatus = namedtuple('CastStatus', ['is_active_input', 'is_stand_by', 'volume_level', 'volume_muted', 'app_id', 'display_name', 'namespaces', 'session_id', 'transport_id', 'status_text']) LaunchFailure = namedtuple('LaunchStatus', ['reason', 'app_id', 'request_id']) # pylint: disable=too-many-instance-attributes class SocketClient(threading.Thread): """ Class to interact with a Chromecast through a socket. :param port: The port to use when connecting to the device, set to None to use the default of 8009. Special devices such as Cast Groups may return a different port number so we need to use that. :param cast_type: The type of chromecast to connect to, see dial.CAST_TYPE_* for types. :param tries: Number of retries to perform if the connection fails. None for inifinite retries. :param retry_wait: A floating point number specifying how many seconds to wait between each retry. None means to use the default which is 5 seconds. """ def __init__(self, host, port=None, cast_type=CAST_TYPE_CHROMECAST, **kwargs): tries = kwargs.pop('tries', None) timeout = kwargs.pop('timeout', None) retry_wait = kwargs.pop('retry_wait', None) self.blocking = kwargs.pop('blocking', True) if self.blocking: self.polltime = POLL_TIME_BLOCKING else: self.polltime = POLL_TIME_NON_BLOCKING super(SocketClient, self).__init__() self.daemon = True self.logger = logging.getLogger(__name__) self._force_recon = False self.cast_type = cast_type self.tries = tries self.timeout = timeout or TIMEOUT_TIME self.retry_wait = retry_wait or RETRY_TIME self.host = host self.port = port or 8009 self.source_id = "sender-0" self.stop = threading.Event() self.app_namespaces = [] self.destination_id = None self.session_id = None self._request_id = 0 # dict mapping requestId on threading.Event objects self._request_callbacks = {} self._open_channels = [] self.connecting = True self.socket = None # dict mapping namespace on Controller objects self._handlers = {} self._connection_listeners = [] self.receiver_controller = ReceiverController(cast_type, self.blocking) self.media_controller = MediaController() self.heartbeat_controller = HeartbeatController() self.register_handler(self.heartbeat_controller) self.register_handler(ConnectionController()) self.register_handler(self.receiver_controller) self.register_handler(self.media_controller) self.receiver_controller.register_status_listener(self) try: self.initialize_connection() except ChromecastConnectionError: self._report_connection_status( ConnectionStatus(CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port))) raise def initialize_connection(self): """Initialize a socket to a Chromecast, retrying as necessary.""" tries = self.tries if self.socket is not None: self.socket.close() self.socket = None # Make sure nobody is blocking. for callback in self._request_callbacks.values(): callback['event'].set() self.app_namespaces = [] self.destination_id = None self.session_id = None self._request_id = 0 self._request_callbacks = {} self._open_channels = [] self.connecting = True retry_log_fun = self.logger.error while not self.stop.is_set() and (tries is None or tries > 0): try: self.socket = ssl.wrap_socket(socket.socket()) self.socket.settimeout(self.timeout) self._report_connection_status( ConnectionStatus(CONNECTION_STATUS_CONNECTING, NetworkAddress(self.host, self.port))) self.socket.connect((self.host, self.port)) self.connecting = False self._force_recon = False self._report_connection_status( ConnectionStatus(CONNECTION_STATUS_CONNECTED, NetworkAddress(self.host, self.port))) self.receiver_controller.update_status() self.heartbeat_controller.ping() self.heartbeat_controller.reset() self.logger.debug("Connected!") break except socket.error as err: self.connecting = True if self.stop.is_set(): self.logger.error( "Failed to connect: %s. aborting due to stop signal.", err) raise ChromecastConnectionError("Failed to connect") self._report_connection_status( ConnectionStatus(CONNECTION_STATUS_FAILED, NetworkAddress(self.host, self.port))) retry_log_fun("Failed to connect, retrying in %.1fs", self.retry_wait) retry_log_fun = self.logger.debug time.sleep(self.retry_wait) if tries: tries -= 1 else: self.stop.set() self.logger.error("Failed to connect. No retries.") raise ChromecastConnectionError("Failed to connect") def disconnect(self): """ Disconnect socket connection to Chromecast device """ self.stop.set() def register_handler(self, handler): """ Register a new namespace handler. """ self._handlers[handler.namespace] = handler handler.registered(self) def new_cast_status(self, cast_status): """ Called when a new cast status has been received. """ new_channel = self.destination_id != cast_status.transport_id if new_channel: self._disconnect_channel(self.destination_id) self.app_namespaces = cast_status.namespaces self.destination_id = cast_status.transport_id self.session_id = cast_status.session_id if new_channel: # If any of the namespaces of the new app are supported # we will automatically connect to it to receive updates for namespace in self.app_namespaces: if namespace in self._handlers: self._ensure_channel_connected(self.destination_id) self._handlers[namespace].channel_connected() def _gen_request_id(self): """ Generates a unique request id. """ self._request_id += 1 return self._request_id @property def is_connected(self): """ Returns True if the client is connected, False if it is stopped (or trying to connect). """ return not self.connecting @property def is_stopped(self): """ Returns True if the connection has been stopped, False if it is running. """ return self.stop.is_set() def run(self): """ Start polling the socket. """ self.heartbeat_controller.reset() self._force_recon = False logging.debug("Thread started...") while not self.stop.is_set(): if self.run_once() == 1: break # Clean up self._cleanup() def run_once(self): """ Use run_once() in your own main loop after you receive something on the socket (get_socket()). """ # pylint: disable=too-many-branches try: if not self._check_connection(): return 0 except ChromecastConnectionError: return 1 # poll the socket can_read, _, _ = select.select([self.socket], [], [], self.polltime) # read messages from chromecast message = data = None if self.socket in can_read and not self._force_recon: try: message = self._read_message() except InterruptLoop as exc: if self.stop.is_set(): self.logger.info( "Stopped while reading message, disconnecting.") else: self.logger.error( "Interruption caught without being stopped: %s", exc) return 1 except ssl.SSLError as exc: if exc.errno == ssl.SSL_ERROR_EOF: if self.stop.is_set(): return 1 raise except socket.error: self._force_recon = True self.logger.error('Error reading from socket.') else: data = _json_from_message(message) if not message: return 0 # If we are stopped after receiving a message we skip the message # and tear down the connection if self.stop.is_set(): return 1 # See if any handlers will accept this message self._route_message(message, data) if REQUEST_ID in data: callback = self._request_callbacks.pop(data[REQUEST_ID], None) if callback is not None: event = callback['event'] callback['response'] = data function = callback['function'] event.set() if function: function(data) def get_socket(self): """ Returns the socket of the connection to use it in you own main loop. """ return self.socket def _check_connection(self): """ Checks if the connection is active, and if not reconnect :return: True if the connection is active, False if the connection was reset. """ # check if connection is expired reset = False if self._force_recon: self.logger.warning( "Error communicating with socket, resetting connection") reset = True elif self.heartbeat_controller.is_expired(): self.logger.warning("Heartbeat timeout, resetting connection") reset = True if reset: self._report_connection_status( ConnectionStatus(CONNECTION_STATUS_LOST, NetworkAddress(self.host, self.port))) try: self.initialize_connection() except ChromecastConnectionError: self.stop.set() return False return True def _route_message(self, message, data): """ Route message to any handlers on the message namespace """ # route message to handlers if message.namespace in self._handlers: # debug messages if message.namespace != NS_HEARTBEAT: self.logger.debug( "Received: %s", _message_to_string(message, data)) # message handlers try: handled = \ self._handlers[message.namespace].receive_message( message, data) if not handled: if data.get(REQUEST_ID) not in self._request_callbacks: self.logger.debug( "Message unhandled: %s", _message_to_string(message, data)) except Exception: # pylint: disable=broad-except self.logger.exception( (u"Exception caught while sending message to " u"controller %s: %s"), type(self._handlers[message.namespace]).__name__, _message_to_string(message, data)) else: self.logger.debug( "Received unknown namespace: %s", _message_to_string(message, data)) def _cleanup(self): """ Cleanup open channels and handlers """ for channel in self._open_channels: try: self._disconnect_channel(channel) except Exception: # pylint: disable=broad-except pass for handler in self._handlers.values(): try: handler.tear_down() except Exception: # pylint: disable=broad-except pass self.socket.close() self._report_connection_status( ConnectionStatus(CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port))) self.connecting = True def _report_connection_status(self, status): """ Report a change in the connection status to any listeners """ for listener in self._connection_listeners: try: self.logger.debug("connection listener: %x (%s)", id(listener), type(listener).__name__) listener.new_connection_status(status) except Exception: # pylint: disable=broad-except pass def _read_bytes_from_socket(self, msglen): """ Read bytes from the socket. """ chunks = [] bytes_recd = 0 while bytes_recd < msglen: if self.stop.is_set(): raise InterruptLoop("Stopped while reading from socket") try: chunk = self.socket.recv(min(msglen - bytes_recd, 2048)) if chunk == b'': raise socket.error("socket connection broken") chunks.append(chunk) bytes_recd += len(chunk) except socket.timeout: continue except ssl.SSLError as exc: # Support older ssl implementations which does not raise # socket.timeout on timeouts if _is_ssl_timeout(exc): continue raise return b''.join(chunks) def _read_message(self): """ Reads a message from the socket and converts it to a message. """ # first 4 bytes is Big-Endian payload length payload_info = self._read_bytes_from_socket(4) read_len = unpack(">I", payload_info)[0] # now read the payload payload = self._read_bytes_from_socket(read_len) # pylint: disable=no-member message = cast_channel_pb2.CastMessage() message.ParseFromString(payload) return message # pylint: disable=too-many-arguments def send_message(self, destination_id, namespace, data, inc_session_id=False, callback_function=False, no_add_request_id=False, force=False): """ Send a message to the Chromecast. """ # namespace is a string containing namespace # data is a dict that will be converted to json # wait_for_response only works if we have a request id # If channel is not open yet, connect to it. self._ensure_channel_connected(destination_id) request_id = None if not no_add_request_id: request_id = self._gen_request_id() data[REQUEST_ID] = request_id if inc_session_id: data[SESSION_ID] = self.session_id # pylint: disable=no-member msg = cast_channel_pb2.CastMessage() msg.protocol_version = msg.CASTV2_1_0 msg.source_id = self.source_id msg.destination_id = destination_id msg.payload_type = cast_channel_pb2.CastMessage.STRING msg.namespace = namespace msg.payload_utf8 = _json_to_payload(data) # prepend message with Big-Endian 4 byte payload size be_size = pack(">I", msg.ByteSize()) # Log all messages except heartbeat if msg.namespace != NS_HEARTBEAT: self.logger.debug("Sending: %s", _message_to_string(msg, data)) if not force and self.stop.is_set(): raise PyChromecastStopped("Socket client's thread is stopped.") if not self.connecting and not self._force_recon: try: self.socket.sendall(be_size + msg.SerializeToString()) except socket.error: self._force_recon = True self.logger.info('Error writing to socket.') else: raise NotConnected("Chromecast is connecting...") if not no_add_request_id and callback_function: self._request_callbacks[request_id] = { 'event': threading.Event(), 'response': None, 'function': callback_function, } def send_platform_message(self, namespace, message, inc_session_id=False, callback_function_param=False): """ Helper method to send a message to the platform. """ return self.send_message(PLATFORM_DESTINATION_ID, namespace, message, inc_session_id, callback_function_param) def send_app_message(self, namespace, message, inc_session_id=False, callback_function_param=False): """ Helper method to send a message to current running app. """ if namespace not in self.app_namespaces: raise UnsupportedNamespace( ("Namespace {} is not supported by current app. " "Supported are {}").format(namespace, ", ".join(self.app_namespaces))) return self.send_message(self.destination_id, namespace, message, inc_session_id, callback_function_param) def register_connection_listener(self, listener): """ Register a connection listener for when the socket connection changes. Listeners will be called with listener.new_connection_status(status) """ self._connection_listeners.append(listener) def _ensure_channel_connected(self, destination_id): """ Ensure we opened a channel to destination_id. """ if destination_id not in self._open_channels: self._open_channels.append(destination_id) self.send_message( destination_id, NS_CONNECTION, {MESSAGE_TYPE: TYPE_CONNECT, 'origin': {}, 'userAgent': 'PyChromecast', 'senderInfo': { 'sdkType': 2, 'version': '15.605.1.3', 'browserVersion': "44.0.2403.30", 'platform': 4, 'systemVersion': 'Macintosh; Intel Mac OS X10_10_3', 'connectionType': 1}}, no_add_request_id=True) def _disconnect_channel(self, destination_id): """ Disconnect a channel with destination_id. """ if destination_id in self._open_channels: self.send_message( destination_id, NS_CONNECTION, {MESSAGE_TYPE: TYPE_CLOSE, 'origin': {}}, no_add_request_id=True, force=True) self._open_channels.remove(destination_id) self.handle_channel_disconnected() def handle_channel_disconnected(self): """ Handles a channel being disconnected. """ for namespace in self.app_namespaces: if namespace in self._handlers: self._handlers[namespace].channel_disconnected() self.app_namespaces = [] self.destination_id = None self.session_id = None class ConnectionController(BaseController): """ Controller to respond to connection messages. """ def __init__(self): super(ConnectionController, self).__init__(NS_CONNECTION) def receive_message(self, message, data): """ Called when a connection message is received. """ if self._socket_client.is_stopped: return True if data[MESSAGE_TYPE] == TYPE_CLOSE: self._socket_client.handle_channel_disconnected() return True else: return False class HeartbeatController(BaseController): """ Controller to respond to heartbeat messages. """ def __init__(self): super(HeartbeatController, self).__init__( NS_HEARTBEAT, target_platform=True) self.last_ping = 0 self.last_pong = time.time() def receive_message(self, message, data): """ Called when a heartbeat message is received. """ if self._socket_client.is_stopped: return True if data[MESSAGE_TYPE] == TYPE_PING: try: self._socket_client.send_message( PLATFORM_DESTINATION_ID, self.namespace, {MESSAGE_TYPE: TYPE_PONG}, no_add_request_id=True) except PyChromecastStopped: self._socket_client.logger.debug( "Heartbeat error when sending response, " "Chromecast connection has stopped") return True elif data[MESSAGE_TYPE] == TYPE_PONG: self.reset() return True else: return False def ping(self): """ Send a ping message. """ self.last_ping = time.time() try: self.send_message({MESSAGE_TYPE: TYPE_PING}) except NotConnected: self._socket_client.logger.error("Chromecast is disconnected. " + "Cannot ping until reconnected.") def reset(self): """ Reset expired counter. """ self.last_pong = time.time() def is_expired(self): """ Indicates if connection has expired. """ if time.time() - self.last_ping > HB_PING_TIME: self.ping() return (time.time() - self.last_pong) > HB_PING_TIME + HB_PONG_TIME class ReceiverController(BaseController): """ Controller to interact with the Chromecast platform. :param cast_type: Type of Chromecast device. """ def __init__(self, cast_type=CAST_TYPE_CHROMECAST, blocking=True): super(ReceiverController, self).__init__( NS_RECEIVER, target_platform=True) self.status = None self.launch_failure = None self.app_to_launch = None self.cast_type = cast_type self.blocking = blocking self.app_launch_event = threading.Event() self.app_launch_event_function = None self._status_listeners = [] self._launch_error_listeners = [] @property def app_id(self): """ Convenience method to retrieve current app id. """ return self.status.app_id if self.status else None def receive_message(self, message, data): """ Called when a receiver-message has been received. """ if data[MESSAGE_TYPE] == TYPE_RECEIVER_STATUS: self._process_get_status(data) return True elif data[MESSAGE_TYPE] == TYPE_LAUNCH_ERROR: self._process_launch_error(data) return True else: return False def register_status_listener(self, listener): """ Register a status listener for when a new Chromecast status has been received. Listeners will be called with listener.new_cast_status(status) """ self._status_listeners.append(listener) def register_launch_error_listener(self, listener): """ Register a listener for when a new launch error message has been received. Listeners will be called with listener.new_launch_error(launch_failure) """ self._launch_error_listeners.append(listener) def update_status(self, callback_function_param=False): """ Sends a message to the Chromecast to update the status. """ self.logger.debug("Receiver:Updating status") self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS}, callback_function=callback_function_param) def launch_app(self, app_id, force_launch=False, callback_function=False): """ Launches an app on the Chromecast. Will only launch if it is not currently running unless force_launch=True. """ if not force_launch and self.app_id is None: self.update_status(lambda response: self._send_launch_message(app_id, force_launch, callback_function)) else: self._send_launch_message(app_id, force_launch, callback_function) def _send_launch_message(self, app_id, force_launch=False, callback_function=False): if force_launch or self.app_id != app_id: self.logger.info("Receiver:Launching app %s", app_id) self.app_to_launch = app_id self.app_launch_event.clear() self.app_launch_event_function = callback_function self.launch_failure = None self.send_message({MESSAGE_TYPE: TYPE_LAUNCH, APP_ID: app_id}, callback_function=lambda response: self._block_till_launched(app_id)) else: self.logger.info( "Not launching app %s - already running", app_id) if callback_function: callback_function() def _block_till_launched(self, app_id): if self.blocking: self.app_launch_event.wait() if self.launch_failure: raise LaunchError( "Failed to launch app: {}, Reason: {}".format( app_id, self.launch_failure.reason)) def stop_app(self, callback_function_param=False): """ Stops the current running app on the Chromecast. """ self.logger.info("Receiver:Stopping current app '%s'", self.app_id) return self.send_message( {MESSAGE_TYPE: 'STOP'}, inc_session_id=True, callback_function=callback_function_param) def set_volume(self, volume): """ Allows to set volume. Should be value between 0..1. Returns the new volume. """ volume = min(max(0, volume), 1) self.logger.info("Receiver:setting volume to %.1f", volume) self.send_message({MESSAGE_TYPE: 'SET_VOLUME', 'volume': {'level': volume}}) return volume def set_volume_muted(self, muted): """ Allows to mute volume. """ self.send_message( {MESSAGE_TYPE: 'SET_VOLUME', 'volume': {'muted': muted}}) @staticmethod def _parse_status(data, cast_type): """ Parses a STATUS message and returns a CastStatus object. :type data: dict :param cast_type: Type of Chromecast. :rtype: CastStatus """ data = data.get('status', {}) volume_data = data.get('volume', {}) try: app_data = data['applications'][0] except KeyError: app_data = {} is_audio = cast_type in (CAST_TYPE_AUDIO, CAST_TYPE_GROUP) status = CastStatus( data.get('isActiveInput', None if is_audio else False), data.get('isStandBy', None if is_audio else True), volume_data.get('level', 1.0), volume_data.get('muted', False), app_data.get(APP_ID), app_data.get('displayName'), [item['name'] for item in app_data.get('namespaces', [])], app_data.get(SESSION_ID), app_data.get('transportId'), app_data.get('statusText', '') ) return status def _process_get_status(self, data): """ Processes a received STATUS message and notifies listeners. """ status = self._parse_status(data, self.cast_type) is_new_app = self.app_id != status.app_id and self.app_to_launch self.status = status self.logger.debug("Received status: %s", self.status) self._report_status() if is_new_app and self.app_to_launch == self.app_id: self.app_to_launch = None self.app_launch_event.set() if self.app_launch_event_function: self.logger.debug("Start app_launch_event_function...") self.app_launch_event_function() self.app_launch_event_function = None def _report_status(self): """ Reports the current status to all listeners. """ for listener in self._status_listeners: try: listener.new_cast_status(self.status) except Exception: # pylint: disable=broad-except pass @staticmethod def _parse_launch_error(data): """ Parses a LAUNCH_ERROR message and returns a LaunchFailure object. :type data: dict :rtype: LaunchFailure """ return LaunchFailure( data.get(ERROR_REASON, None), data.get(APP_ID), data.get(REQUEST_ID), ) def _process_launch_error(self, data): """ Processes a received LAUNCH_ERROR message and notifies listeners. """ launch_failure = self._parse_launch_error(data) self.launch_failure = launch_failure if self.app_to_launch: self.app_to_launch = None self.app_launch_event.set() self.logger.debug("Launch status: %s", launch_failure) for listener in self._launch_error_listeners: try: listener.new_launch_error(launch_failure) except Exception: # pylint: disable=broad-except pass def tear_down(self): """ Called when controller is destroyed. """ super(ReceiverController, self).tear_down() self.status = None self.launch_failure = None self.app_to_launch = None self.app_launch_event.clear() self._report_status() self._status_listeners[:] = [] pychromecast-0.8.1/pylintrc000066400000000000000000000002571306355002200160100ustar00rootroot00000000000000[MASTER] ignore=cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py reports=no disable=locally-disabled [EXCEPTIONS] overgeneral-exceptions=Exception,PyChromecastError pychromecast-0.8.1/requirements.txt000066400000000000000000000000731306355002200175010ustar00rootroot00000000000000requests>=2.0 protobuf>=3.0.0 zeroconf>=0.17.7 six>=1.10.0 pychromecast-0.8.1/setup.cfg000066400000000000000000000000261306355002200160340ustar00rootroot00000000000000[wheel] universal = 1 pychromecast-0.8.1/setup.py000066400000000000000000000016601306355002200157320ustar00rootroot00000000000000from setuptools import setup, find_packages long_description = open('README.rst').read() setup( name='PyChromecast', version='0.8.1', license='MIT', url='https://github.com/balloob/pychromecast', author='Paulus Schoutsen', author_email='paulus@paulusschoutsen.nl', description='Python module to talk to Google Chromecast.', long_description=long_description, packages=find_packages(), zip_safe=False, include_package_data=True, platforms='any', install_requires=list(val.strip() for val in open('requirements.txt')), classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries :: Python Modules' ] )