pax_global_header00006660000000000000000000000064141432460550014516gustar00rootroot0000000000000052 comment=a4f9d23159cf861c018866e7004a816aa9bce9ae pychromecast-9.4.0/000077500000000000000000000000001414324605500142315ustar00rootroot00000000000000pychromecast-9.4.0/.github/000077500000000000000000000000001414324605500155715ustar00rootroot00000000000000pychromecast-9.4.0/.github/dependabot.yml000066400000000000000000000001751414324605500204240ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 pychromecast-9.4.0/.github/release-drafter.yml000066400000000000000000000000541414324605500213600ustar00rootroot00000000000000template: | ## What's Changed $CHANGES pychromecast-9.4.0/.github/workflows/000077500000000000000000000000001414324605500176265ustar00rootroot00000000000000pychromecast-9.4.0/.github/workflows/pythonpublish.yml000066400000000000000000000015151414324605500232630ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* pychromecast-9.4.0/.github/workflows/release-drafter.yml000066400000000000000000000005101414324605500234120ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master jobs: update_release_draft: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pychromecast-9.4.0/.github/workflows/test.yml000066400000000000000000000017461414324605500213400ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Run Tests on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v1 with: python-version: 3.8 - name: Install dependencies run: | pip install -r requirements.txt pip install -r requirements-test.txt - name: Lint with flake8 run: | flake8 --exclude cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py examples pychromecast - name: Lint with pylint run: | pylint examples pychromecast - name: Check formatting with black run: | black examples pychromecast --check pychromecast-9.4.0/.gitignore000066400000000000000000000007741414324605500162310ustar00rootroot00000000000000# 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-9.4.0/LICENSE000066400000000000000000000020731414324605500152400ustar00rootroot00000000000000The 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-9.4.0/MANIFEST.in000066400000000000000000000001541414324605500157670ustar00rootroot00000000000000include README.rst include LICENSE include requirements.txt graft pychromecast recursive-exclude * *.py[co] pychromecast-9.4.0/README.rst000066400000000000000000000157641414324605500157350ustar00rootroot00000000000000pychromecast |Build Status| =========================== .. |Build Status| image:: https://travis-ci.org/balloob/pychromecast.svg?branch=master :target: https://travis-ci.org/balloob/pychromecast Library for Python 3.6+ 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 `_ *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`` How to use ---------- .. code:: python >> import time >> import pychromecast >> # List chromecasts on the network, but don't connect >> services, browser = pychromecast.discovery.discover_chromecasts() >> # Shut down discovery >> pychromecast.discovery.stop_discovery(browser) >> # Discover and connect to chromecasts named Living Room >> chromecasts, browser = pychromecast.get_listed_chromecasts(friendly_names=["Living Room"]) >> [cc.device.friendly_name for cc in chromecasts] ['Living Room'] >> cast = chromecasts[0] >> # Start worker thread and wait for cast device to be ready >> cast.wait() >> print(cast.device) DeviceStatus(friendly_name='Living Room', model_name='Chromecast', manufacturer='Google Inc.', 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='CC1AD845', display_name='Default Media Receiver', namespaces=['urn:x-cast:com.google.cast.player.message', 'urn:x-cast:com.google.cast.media'], session_id='CCA39713-9A4F-34A6-A8BF-5D97BE7ECA5C', transport_id='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='http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', content_type='video/mp4', duration=596.474195, stream_type='BUFFERED', idle_reason=None, media_session_id=1, playback_rate=1, player_state='PLAYING', supported_media_commands=15, volume_level=1, volume_muted=False) >> mc.pause() >> time.sleep(5) >> mc.play() >> # Shut down discovery >> pychromecast.discovery.stop_discovery(browser) 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-export/` * Select 'Include raw bytes (will include cookies and credentials)' * Click 'Start Logging to Disk' * 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. * Open https://netlog-viewer.appspot.com/ and select your event log file. * Browse to https://netlog-viewer.appspot.com/#events&q=type:SOCKET, and find the socket that has familiar JSON data. (For me, it's usually the second or third from the top.) * 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 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-9.4.0/chromecast_protobuf/000077500000000000000000000000001414324605500203015ustar00rootroot00000000000000pychromecast-9.4.0/chromecast_protobuf/README.md000066400000000000000000000002351414324605500215600ustar00rootroot00000000000000These files were imported from https://chromium.googlesource.com/chromium/src.git/+/master/extensions/common/api/cast_channel to generate the \_pb2.py-files.pychromecast-9.4.0/chromecast_protobuf/authority_keys.proto000066400000000000000000000006311414324605500244510ustar00rootroot00000000000000// 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-9.4.0/chromecast_protobuf/cast_channel.proto000066400000000000000000000057001414324605500240120ustar00rootroot00000000000000// 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-9.4.0/chromecast_protobuf/logging.proto000066400000000000000000000120521414324605500230140ustar00rootroot00000000000000// 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-9.4.0/examples/000077500000000000000000000000001414324605500160475ustar00rootroot00000000000000pychromecast-9.4.0/examples/bbciplayer_example.py000066400000000000000000000050761414324605500222600ustar00rootroot00000000000000""" Example on how to use the BBC iPlayer Controller """ # pylint: disable=invalid-name import argparse import logging import sys from time import sleep import json import zeroconf import pychromecast from pychromecast import quick_play # Change to the name of your Chromecast CAST_NAME = "Lounge Video" # Note: Media ID is NOT the 8 digit alpha-numeric in the URL # it can be found by right clicking the playing video on the web interface # e.g. https://www.bbc.co.uk/iplayer/episode/b09w7fd9/bitz-bob-series-1-1-castle-makeover shows: # "2908kbps | dash (mf_cloudfront_dash_https) # b09w70r2 | 960x540" MEDIA_ID = "b09w70r2" IS_LIVE = False METADATA = { "metadatatype": 0, "title": "Bitz & Bob", "subtitle": "Castle Makeover", "images": [{"url": "https://ichef.bbci.co.uk/images/ic/1280x720/p07j4m3r.jpg"}], } parser = argparse.ArgumentParser( description="Example on how to use the BBC iPlayer Controller to play an media stream." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--media_id", help='MediaID (default: "%(default)s")', default=MEDIA_ID ) parser.add_argument( "--metadata", help='Metadata (default: "%(default)s")', default=json.dumps(METADATA) ) parser.add_argument( "--is_live", help="Show 'live' and no current/end timestamps on UI", action="store_true", default=IS_LIVE, ) args = parser.parse_args() app_name = "bbciplayer" app_data = { "media_id": args.media_id, "is_live": args.is_live, "metadata": json.loads(args.metadata), } if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() print(f'Found chromecast with name "{args.cast}", attempting to play "{args.media_id}"') quick_play.quick_play(cast, app_name, app_data) sleep(10) browser.stop_discovery() pychromecast-9.4.0/examples/bbcsounds_example.py000066400000000000000000000046061414324605500221240ustar00rootroot00000000000000""" Example on how to use the BBC iPlayer Controller """ # pylint: disable=invalid-name import argparse import logging import sys from time import sleep import json import zeroconf import pychromecast from pychromecast import quick_play # Change to the name of your Chromecast CAST_NAME = "Lounge Video" # Media ID can be found in the URL # e.g. https://www.bbc.co.uk/sounds/live:bbc_radio_one MEDIA_ID = "bbc_radio_one" IS_LIVE = True METADATA = { "metadatatype": 0, "title": "Radio 1", "images": [ { "url": "https://sounds.files.bbci.co.uk/2.3.0/networks/bbc_radio_one/background_1280x720.png" } ], } parser = argparse.ArgumentParser( description="Example on how to use the BBC Sounds Controller to play an media stream." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--media_id", help='MediaID (default: "%(default)s")', default=MEDIA_ID ) parser.add_argument( "--metadata", help='Metadata (default: "%(default)s")', default=json.dumps(METADATA) ) parser.add_argument( "--is_live", help="Show 'live' and no current/end timestamps on UI", action="store_true", default=IS_LIVE, ) args = parser.parse_args() app_name = "bbcsounds" app_data = { "media_id": args.media_id, "is_live": args.is_live, "metadata": json.loads(args.metadata), } if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() print(f'Found chromecast with name "{args.cast}", attempting to play "{args.media_id}"') quick_play.quick_play(cast, app_name, app_data) sleep(10) browser.stop_discovery() pychromecast-9.4.0/examples/bubbleupnp_example.py000066400000000000000000000037001414324605500222720ustar00rootroot00000000000000""" Example on how to use the BubbleUPNP Controller to play an URL. """ # pylint: disable=invalid-name import argparse import logging import sys from time import sleep import zeroconf import pychromecast from pychromecast.controllers.bubbleupnp import BubbleUPNPController # Change to the friendly name of your Chromecast CAST_NAME = "Kitchen speaker" # Change to an audio or video url MEDIA_URL = "https://c3.toivon.net/toivon/toivon_3?mp=/stream" parser = argparse.ArgumentParser( description="Example on how to use the BubbleUPNP Controller to play an URL." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--url", help='Media url (default: "%(default)s")', default=MEDIA_URL ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) # pylint: disable=unbalanced-tuple-unpacking chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = list(chromecasts)[0] # Start socket client's worker thread and wait for initial status update cast.wait() print(f'Found chromecast with name "{args.cast}", attempting to play "{args.url}"') bubbleupnp = BubbleUPNPController() cast.register_handler(bubbleupnp) bubbleupnp.launch() bubbleupnp.play_media(args.url, "audio/mp3", stream_type="LIVE") sleep(10) browser.stop_discovery() pychromecast-9.4.0/examples/custom_loop.py000066400000000000000000000061311414324605500207650ustar00rootroot00000000000000""" Example that shows how the socket client can be used without its own worker thread by not calling Chromecast.start() or Chromecast.wait(), but instead calling Chromecast.connect(). You can use that functionality to include pychromecast into your main loop. """ # pylint: disable=invalid-name import argparse import logging import select import time import zeroconf import pychromecast CAST_NAME = "Living Room" def your_main_loop(): """ Main loop example. Check for cast.socket_client.get_socket() and handle it with cast.socket_client.run_once() """ t = 1 cast = None def callback(chromecast): if chromecast.name == args.cast: print("=> Discovered cast...") chromecast.connect() nonlocal cast cast = chromecast browser = 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(f"=> Waiting for discovery of cast '{args.cast}'...") time.sleep(1) print("All done, shutting down discovery") browser.stop_discovery() def do_actions(cast, t): """Your code which is called by main loop.""" 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) parser = argparse.ArgumentParser(description="Example without socket_client thread") parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) your_main_loop() pychromecast-9.4.0/examples/dashcast_example.py000066400000000000000000000044271414324605500217350ustar00rootroot00000000000000""" Example that shows how the DashCast controller can be used. """ # pylint: disable=invalid-name import argparse import logging import sys import time import zeroconf import pychromecast from pychromecast.controllers import dashcast # Change to the friendly name of your Chromecast CAST_NAME = "Living Room" parser = argparse.ArgumentParser( description="Example that shows how the DashCast controller can be used." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() d = dashcast.DashCastController() cast.register_handler(d) print() print(cast.device) time.sleep(1) print() print(cast.status) print() print(cast.media_controller.status) print() if not cast.is_idle: print("Killing current running app") cast.quit_app() t = 5 while cast.status.app_id is not None and t > 0: time.sleep(0.1) t = t - 0.1 time.sleep(1) # Test that the callback chain works. This should send a message to # load the first url, but immediately after send a message load the # second url. warning_message = "If you see this on your TV then something is broken" d.load_url( "https://home-assistant.io/? " + warning_message, callback_function=lambda result: d.load_url("https://home-assistant.io/"), ) # If debugging, sleep after running so we can see any error messages. if args.show_debug: time.sleep(10) # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/discovery_example.py000066400000000000000000000043141414324605500221450ustar00rootroot00000000000000""" Example that shows how to receive updates on discovered chromecasts. """ # pylint: disable=invalid-name import argparse import logging import time import zeroconf import pychromecast parser = argparse.ArgumentParser( description="Example on how to receive updates on discovered chromecasts." ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument( "--force-zeroconf", help="Zeroconf will be used even if --known-host is present", action="store_true", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) def list_devices(): """Print a list of known devices.""" print("Currently known cast devices:") for uuid, service in browser.services.items(): print(f" {uuid} {service}") class MyCastListener(pychromecast.discovery.AbstractCastListener): """Listener for discovering chromecasts.""" def add_cast(self, uuid, _service): """Called when a new cast has beeen discovered.""" print(f"Found cast device with UUID {uuid}") list_devices() def remove_cast(self, uuid, _service, cast_info): """Called when a cast has beeen lost (MDNS info expired or host down).""" print(f"Lost cast device with UUID {uuid} {cast_info}") list_devices() def update_cast(self, uuid, _service): """Called when a cast has beeen updated (MDNS info renewed or changed).""" print(f"Updated cast device with UUID {uuid}") list_devices() if args.known_host and not args.force_zeroconf: zconf = None else: zconf = zeroconf.Zeroconf() browser = pychromecast.discovery.CastBrowser(MyCastListener(), zconf, args.known_host) browser.start_discovery() try: while True: time.sleep(1) except KeyboardInterrupt: pass # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/discovery_example2.py000066400000000000000000000020551414324605500222270ustar00rootroot00000000000000""" Example that shows how to list all available chromecasts. """ # pylint: disable=invalid-name import argparse import logging import zeroconf import pychromecast parser = argparse.ArgumentParser( description="Example that shows how to list all available chromecasts." ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) devices, browser = pychromecast.discovery.discover_chromecasts( known_hosts=args.known_host ) # Shut down discovery browser.stop_discovery() print(f"Discovered {len(devices)} device(s):") for device in devices: print(f" {device}") pychromecast-9.4.0/examples/discovery_example3.py000066400000000000000000000030451414324605500222300ustar00rootroot00000000000000""" Example that shows how to list chromecasts matching on name or uuid. """ # pylint: disable=invalid-name import argparse import logging import sys from uuid import UUID import zeroconf import pychromecast parser = argparse.ArgumentParser( description="Example that shows how to list chromecasts matching on name or uuid." ) parser.add_argument("--cast", help='Name of wanted cast device")', default=None) parser.add_argument("--uuid", help="UUID of wanted cast device", default=None) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) if args.cast is None and args.uuid is None: print("Need to supply `cast` or `uuid`") sys.exit(1) friendly_names = [] if args.cast: friendly_names.append(args.cast) uuids = [] if args.uuid: uuids.append(UUID(args.uuid)) devices, browser = pychromecast.discovery.discover_listed_chromecasts( friendly_names=friendly_names, uuids=uuids, known_hosts=args.known_host ) # Shut down discovery browser.stop_discovery() print(f"Discovered {len(devices)} device(s):") for device in devices: print(f" {device}") pychromecast-9.4.0/examples/get_chromecasts.py000066400000000000000000000023121414324605500215710ustar00rootroot00000000000000""" Example that shows how to connect to all chromecasts. """ # pylint: disable=invalid-name import argparse import logging import sys import zeroconf import pychromecast parser = argparse.ArgumentParser( description="Example on how to connect to all chromecasts." ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) casts, browser = pychromecast.get_chromecasts(known_hosts=args.known_host) # Shut down discovery as we don't care about updates browser.stop_discovery() if len(casts) == 0: print("No Devices Found") sys.exit(1) print("Found cast devices:") for cast in casts: print( f' "{cast.name}" on mDNS service {cast._services} with UUID:{cast.uuid}' # pylint: disable=protected-access ) pychromecast-9.4.0/examples/homeassistant_media_example.py000066400000000000000000000036661414324605500241700ustar00rootroot00000000000000""" Example on how to use the Home Assistant Media app to play an URL. """ # pylint: disable=invalid-name import argparse import logging import sys from time import sleep import zeroconf import pychromecast from pychromecast import quick_play # Change to the friendly name of your Chromecast CAST_NAME = "Kitchen speaker" # Change to an audio or video url MEDIA_URL = ( "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" ) parser = argparse.ArgumentParser( description="Example on how to use the Home Asssitant Media Controller to play an URL." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--url", help='Media url (default: "%(default)s")', default=MEDIA_URL ) args = parser.parse_args() app_name = "homeassistant_media" app_data = { "media_id": args.url, } if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) # pylint: disable=unbalanced-tuple-unpacking chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = list(chromecasts)[0] # Start socket client's worker thread and wait for initial status update cast.wait() print(f'Found chromecast with name "{args.cast}", attempting to play "{args.url}"') quick_play.quick_play(cast, app_name, app_data) sleep(10) browser.stop_discovery() pychromecast-9.4.0/examples/media_enqueue.py000066400000000000000000000042341414324605500212320ustar00rootroot00000000000000""" Example on how to use queuing with Media Controller """ # pylint: disable=invalid-name import argparse import logging import sys import time import zeroconf import pychromecast # Change to the friendly name of your Chromecast CAST_NAME = "Living Room" # Change to an audio or video url MEDIA_URLS = [ "https://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/dash/nonuk/dash_low/llnws/bbc_radio_fourfm.mpd", "https://www.bensound.com/bensound-music/bensound-jazzyfrenchy.mp3", "https://audio.guim.co.uk/2020/08/14-65292-200817TIFXR.mp3", ] parser = argparse.ArgumentParser( description="Example on how to use the Media Controller with a queue." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() print(f'Found chromecast with name "{args.cast}"') cast.media_controller.play_media(MEDIA_URLS[0], "audio/mp3") # Wait for Chromecast to start playing while cast.media_controller.status.player_state != "PLAYING": time.sleep(0.1) # Queue next items for URL in MEDIA_URLS[1:]: print("Enqueuing...") cast.media_controller.play_media(URL, "audio/mp3", enqueue=True) for URL in MEDIA_URLS[1:]: time.sleep(5) print("Skipping...") cast.media_controller.queue_next() # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/media_example.py000066400000000000000000000046151414324605500212210ustar00rootroot00000000000000""" Example on how to use the Media Controller to play an URL. """ # pylint: disable=invalid-name import argparse import logging import sys import time import zeroconf import pychromecast # Change to the friendly name of your Chromecast CAST_NAME = "Living Room" # Change to an audio or video url MEDIA_URL = "https://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/dash/nonuk/dash_low/llnws/bbc_radio_fourfm.mpd" parser = argparse.ArgumentParser( description="Example on how to use the Media Controller to play an URL." ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument( "--url", help='Media url (default: "%(default)s")', default=MEDIA_URL ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() print(f'Found chromecast with name "{args.cast}", attempting to play "{args.url}"') cast.media_controller.play_media(args.url, "audio/mp3") # Wait for player_state PLAYING player_state = None t = 30 has_played = False while True: try: if player_state != cast.media_controller.status.player_state: player_state = cast.media_controller.status.player_state print("Player state:", player_state) if player_state == "PLAYING": has_played = True if cast.socket_client.is_connected and has_played and player_state != "PLAYING": has_played = False cast.media_controller.play_media(args.url, "audio/mp3") time.sleep(0.1) t = t - 0.1 except KeyboardInterrupt: break # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/media_example2.py000066400000000000000000000055061414324605500213030ustar00rootroot00000000000000""" Example on how to use the Media Controller. """ # pylint: disable=invalid-name import argparse import logging import sys import time import zeroconf import pychromecast # Change to the friendly name of your Chromecast CAST_NAME = "Living Room" # Change to an audio or video url MEDIA_URL = ( "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" ) parser = argparse.ArgumentParser( description="Example on how to use the Media Controller." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-status-only", help="Show status, then exit", action="store_true" ) parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--url", help='Media url (default: "%(default)s")', default=MEDIA_URL ) args = parser.parse_args() if args.show_debug: fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" datefmt = "%Y-%m-%d %H:%M:%S" logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() print() print(cast.device) time.sleep(1) print() print(cast.status) print() print(cast.media_controller.status) print() if args.show_status_only: sys.exit() if not cast.is_idle: print("Killing current running app") cast.quit_app() t = 5 while cast.status.app_id is not None and t > 0: time.sleep(0.1) t = t - 0.1 print(f'Playing media "{args.url}"') cast.play_media(args.url, "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 == 32: cast.quit_app() break time.sleep(1) except KeyboardInterrupt: break # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/multizone_example.py000066400000000000000000000047551414324605500221750ustar00rootroot00000000000000""" Example on how to use the Multizone (Audio Group) Controller """ # pylint: disable=invalid-name import argparse import logging import sys import time import zeroconf import pychromecast from pychromecast.controllers.multizone import ( MultizoneController, MultiZoneControllerListener, ) from pychromecast.socket_client import ConnectionStatusListener # Change to the name of your Chromecast CAST_NAME = "Whole house" parser = argparse.ArgumentParser( description="Example on how to use the Multizone Controller to track groupp members." ) parser.add_argument( "--cast", help='Name of speaker group (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) class MyConnectionStatusListener(ConnectionStatusListener): """ConnectionStatusListener""" def __init__(self, _mz): self._mz = _mz def new_connection_status(self, status): if status.status == "CONNECTED": self._mz.update_members() class MyMultiZoneControllerListener(MultiZoneControllerListener): """MultiZoneControllerListener""" def multizone_member_added(self, group_uuid): print(f"New member: {group_uuid}") def multizone_member_removed(self, group_uuid): print(f"Removed member: {group_uuid}") def multizone_status_received(self): print(f"Members: {mz.members}") chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Add listeners mz = MultizoneController(cast.uuid) mz.register_listener(MyMultiZoneControllerListener()) cast.register_handler(mz) cast.register_connection_listener(MyConnectionStatusListener(mz)) # Start socket client's worker thread and wait for initial status update cast.wait() while True: try: time.sleep(1) except KeyboardInterrupt: break # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/plex_multi_example.py000066400000000000000000000126761414324605500223320ustar00rootroot00000000000000""" Examples of the Plex controller playing on a Chromecast. DEMO TYPES: * simple: Picks the first item it finds in your libray and plays it. * list: Creates a list of items from your library and plays them. * playqueue: Creates a playqueue and plays it. * playlist: Creates a playlist, plays it, then deletes it. All demos with the exception of 'simple' can use startItem. startItem lets you start playback anywhere in the list of items. turning this option on will pick an item in the middle of the list to start from. This demo uses features that require the latest Python-PlexAPI pip install plexapi """ # pylint: disable=invalid-name import argparse import logging import sys import zeroconf from plexapi.server import PlexServer # pylint: disable=import-error import pychromecast from pychromecast.controllers.plex import PlexController # Change to the friendly name of your Chromecast. CAST_NAME = "Office TV" # Replace with your own Plex URL, including port. PLEX_URL = "http://192.168.1.3:32400" # Replace with your Plex token. See link below on how to find it: # https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ PLEX_TOKEN = "Y0urT0k3nH3rE" # Library of items to pick from for tests. Use "episode", "movie", or "track". PLEX_LIBRARY = "episode" # The demo type you'd like to run. # Options are "single", "list", "playqueue", or "playlist" DEMO_TYPE = "playqueue" # If demo type is anything other than "single", # make this True to see a demo of startItem. START_ITEM = True parser = argparse.ArgumentParser( description="How to play media items, lists, playQueues, " "and playlists to a Chromecast device." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s").', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--url", help='URL of your Plex Server (default: "%(default)s").', default=PLEX_URL ) parser.add_argument( "--library", help="The library you'd like to test: episode, movie, or track (default: '%(default)s').", default=PLEX_LIBRARY, ) parser.add_argument("--token", help="Your Plex token.", default=PLEX_TOKEN) parser.add_argument( "--demo", help="The demo you'd like to run: single, list, playqueue, or playlist (default: '%(default)s').", default=DEMO_TYPE, ) parser.add_argument( "--startitem", help="If demo type is anything other than 'single', set to True to see a demo of startItem (default: '%(default)s').", default=START_ITEM, ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) startItem = None def media_info(_media, items): """Print media info.""" print(f"Cast Device: {cast.name}") print(f"Media Type: {type(_media)}") print(f"Media Items: {items}") def start_item_info(_media): """Print item info.""" if args.startitem: print(f"Starting From: {_media}") chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) cast = next((cc for cc in chromecasts if cc.name == args.cast), None) if not cast: print(f"No chromecast with name '{args.cast}' found.") foundCasts = ", ".join([cc.name for cc in pychromecast.get_chromecasts()[0]]) print(f"Chromecasts found: {foundCasts}") sys.exit(1) plex_server = PlexServer(args.url, args.token) # Create a list of 5 items from the selected library. libraryItems = plex_server.library.search( libtype=args.library, sort="addedAt:desc", limit=5 ) if args.demo == "single": # Use a single item as media. media = libraryItems[0] media_info(media, libraryItems[0]) elif args.demo == "list": # Use the unaltered list as media. media = libraryItems # Set starting position to the 2nd item if startItem demo. startItem = libraryItems[1] if args.startitem else None # Print info media_info(libraryItems, libraryItems) start_item_info(libraryItems[1]) elif args.demo == "playqueue": # Convert list into a playqueue for media. media = plex_server.createPlayQueue(libraryItems) # Set starting position to the 3rd item if startItem demo. startItem = libraryItems[2] if args.startitem else None # Print info media_info(media, media.items) start_item_info(libraryItems[2]) elif args.demo == "playlist": # Convert list into a playlist for media. media = plex_server.createPlaylist("pychromecast test playlist", libraryItems) # Set starting position to the 4th item if startItem demo. startItem = libraryItems[3] if args.startitem else None # Print info media_info(media, media.items()) start_item_info(libraryItems[2]) plex_c = PlexController() cast.register_handler(plex_c) cast.wait() # Plays the media item, list, playlist, or playqueue. # If startItem = None it is ignored and playback starts at first item, # otherwise playback starts at the position of the media item given. plex_c.block_until_playing(media, startItem=startItem) if getattr(media, "TYPE", None) == "playlist": media.delete() # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/simple_listener_example.py000066400000000000000000000047411414324605500233400ustar00rootroot00000000000000""" Example showing how to create a simple Chromecast event listener for device and media status events """ # pylint: disable=invalid-name import argparse import logging import sys import time import zeroconf import pychromecast from pychromecast.controllers.media import MediaStatusListener from pychromecast.controllers.receiver import CastStatusListener # Change to the friendly name of your Chromecast CAST_NAME = "Living Room Speaker" class MyCastStatusListener(CastStatusListener): """Cast status listener""" def __init__(self, name, cast): self.name = name self.cast = cast def new_cast_status(self, status): print("[", time.ctime(), " - ", self.name, "] status chromecast change:") print(status) class MyMediaStatusListener(MediaStatusListener): """Status media listener""" def __init__(self, name, cast): self.name = name self.cast = cast def new_media_status(self, status): print("[", time.ctime(), " - ", self.name, "] status media change:") print(status) parser = argparse.ArgumentParser( description="Example on how to create a simple Chromecast event listener." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) chromecast = chromecasts[0] # Start socket client's worker thread and wait for initial status update chromecast.wait() listenerCast = MyCastStatusListener(chromecast.name, chromecast) chromecast.register_status_listener(listenerCast) listenerMedia = MyMediaStatusListener(chromecast.name, chromecast) chromecast.media_controller.register_status_listener(listenerMedia) input("Listening for Chromecast events...\n\n") # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/supla_example.py000066400000000000000000000022141414324605500212570ustar00rootroot00000000000000""" Example on how to use the Supla Controller """ # pylint: disable=invalid-name import logging from time import sleep import sys import requests from bs4 import BeautifulSoup # pylint: disable=import-error import pychromecast from pychromecast.controllers.supla import SuplaController # Change to the name of your Chromecast CAST_NAME = "Kitchen Speaker" # Change to the video id of the YouTube video # video id is the last part of the url http://youtube.com/watch?v=video_id PROGRAM = "aamulypsy" result = requests.get(f"https://www.supla.fi/ohjelmat/{PROGRAM}") soup = BeautifulSoup(result.content) MEDIA_ID = soup.select('a[title*="Koko Shitti"]')[0]["href"].split("/")[-1] print(MEDIA_ID) logging.basicConfig(level=logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts(friendly_names=[CAST_NAME]) if not chromecasts: print(f'No chromecast with name "{CAST_NAME}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() supla = SuplaController() cast.register_handler(supla) supla.launch() supla.play_media(MEDIA_ID) cast.wait() sleep(10) pychromecast-9.4.0/examples/yleareena_example.py000066400000000000000000000057351414324605500221130ustar00rootroot00000000000000""" Example on how to use the Yle Areena Controller """ # pylint: disable=invalid-name, import-outside-toplevel import argparse import logging import sys from time import sleep import zeroconf import pychromecast from pychromecast.controllers.yleareena import YleAreenaController logger = logging.getLogger(__name__) # Change to the name of your Chromecast CAST_NAME = "My Chromecast" parser = argparse.ArgumentParser( description="Example on how to use the Yle Areena Controller." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument("--program", help="Areena Program ID", default="1-50097921") parser.add_argument("--audio_language", help="audio_language", default="") parser.add_argument("--text_language", help="text_language", default="off") args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) def get_kaltura_id(program_id): """ Dive into the yledl internals and fetch the kaltura player id. This can be used with Chromecast """ # yledl is not available in CI, silence import warnings from yledl.streamfilters import StreamFilters # pylint: disable=import-error from yledl.http import HttpClient # pylint: disable=import-error from yledl.localization import TranslationChooser # pylint: disable=import-error from yledl.extractors import extractor_factory # pylint: disable=import-error from yledl.titleformatter import TitleFormatter # pylint: disable=import-error title_formatter = TitleFormatter() language_chooser = TranslationChooser("fin") httpclient = HttpClient(None) stream_filters = StreamFilters() url = f"https://areena.yle.fi/{program_id}" extractor = extractor_factory(url, stream_filters, language_chooser, httpclient) pid = extractor.program_id_from_url(url) info = extractor.program_info_for_pid(pid, url, title_formatter, None) return info.media_id.split("-")[-1] chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() yt = YleAreenaController() cast.register_handler(yt) yt.play_areena_media( get_kaltura_id(args.program), audio_language=args.audio_language, text_language=args.text_language, ) sleep(10) # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/examples/youtube_example.py000066400000000000000000000033441414324605500216340ustar00rootroot00000000000000""" Example on how to use the YouTube Controller """ # pylint: disable=invalid-name import argparse import logging import sys import zeroconf import pychromecast from pychromecast.controllers.youtube import YouTubeController # Change to the name of your Chromecast CAST_NAME = "Living Room TV" # Change to the video id of the YouTube video # video id is the last part of the url http://youtube.com/watch?v=video_id VIDEO_ID = "dQw4w9WgXcQ" parser = argparse.ArgumentParser( description="Example on how to use the Youtube Controller." ) parser.add_argument( "--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME ) parser.add_argument( "--known-host", help="Add known host (IP), can be used multiple times", action="append", ) parser.add_argument("--show-debug", help="Enable debug log", action="store_true") parser.add_argument( "--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" ) parser.add_argument( "--videoid", help='Youtube video ID (default: "%(default)s")', default=VIDEO_ID ) args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) if args.show_zeroconf_debug: print("Zeroconf version: " + zeroconf.__version__) logging.getLogger("zeroconf").setLevel(logging.DEBUG) chromecasts, browser = pychromecast.get_listed_chromecasts( friendly_names=[args.cast], known_hosts=args.known_host ) if not chromecasts: print(f'No chromecast with name "{args.cast}" discovered') sys.exit(1) cast = chromecasts[0] # Start socket client's worker thread and wait for initial status update cast.wait() yt = YouTubeController() cast.register_handler(yt) yt.play_video(VIDEO_ID) # Shut down discovery browser.stop_discovery() pychromecast-9.4.0/fabfile.py000066400000000000000000000010131414324605500161660ustar00rootroot00000000000000import 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-9.4.0/pychromecast/000077500000000000000000000000001414324605500167325ustar00rootroot00000000000000pychromecast-9.4.0/pychromecast/__init__.py000066400000000000000000000466671414324605500210660ustar00rootroot00000000000000""" PyChromecast: remote control your Chromecast """ import logging import fnmatch from threading import Event import threading import zeroconf from .config import * # noqa: F403 from .error import * # noqa: F403 from . import socket_client from .discovery import ( # noqa: F401 DISCOVER_TIMEOUT, CastBrowser, CastListener, # Deprecated SimpleCastListener, discover_chromecasts, start_discovery, stop_discovery, ) from .dial import get_device_status, DeviceStatus from .const import ( # noqa: F401 CAST_MANUFACTURERS, CAST_TYPE_GROUP, CAST_TYPES, CAST_TYPE_CHROMECAST, ) from .controllers.media import STREAM_TYPE_BUFFERED # noqa: F401 __all__ = ("__version__", "__version_info__", "get_chromecasts", "Chromecast") __version_info__ = ("0", "7", "6") __version__ = ".".join(__version_info__) IDLE_APP_ID = "E8C28D3C" IGNORE_CEC = [] _LOGGER = logging.getLogger(__name__) def get_chromecast_from_host(host, tries=None, retry_wait=None, timeout=None): """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 _LOGGER.debug("get_chromecast_from_host %s", host) cast_type = None manufacturer = None multizone_supported = None if model_name.lower() == "google cast group": cast_type = CAST_TYPE_GROUP manufacturer = "Google Inc." multizone_supported = False device = DeviceStatus( friendly_name=friendly_name, model_name=model_name, manufacturer=manufacturer, uuid=uuid, cast_type=cast_type, multizone_supported=multizone_supported, ) return Chromecast( host=ip_address, port=port, device=device, tries=tries, timeout=timeout, retry_wait=retry_wait, ) # Alias for backwards compatibility _get_chromecast_from_host = get_chromecast_from_host # pylint: disable=invalid-name def get_chromecast_from_cast_info( cast_info, zconf, tries=None, retry_wait=None, timeout=None ): """Creates a Chromecast object from a zeroconf service.""" # Build device status from the CastInfo, this # information is the primary source and the remaining will be # fetched later on. services = cast_info.services _LOGGER.debug("get_chromecast_from_cast_info %s", services) cast_type = None manufacturer = None multizone_supported = None if cast_info.model_name.lower() == "google cast group": cast_type = CAST_TYPE_GROUP manufacturer = "Google Inc." multizone_supported = False device = DeviceStatus( friendly_name=cast_info.friendly_name, model_name=cast_info.model_name, manufacturer=manufacturer, uuid=cast_info.uuid, cast_type=cast_type, multizone_supported=multizone_supported, ) return Chromecast( host=None, device=device, tries=tries, timeout=timeout, retry_wait=retry_wait, services=services, zconf=zconf, ) # Alias for backwards compatibility _get_chromecast_from_service = ( # pylint: disable=invalid-name get_chromecast_from_cast_info ) def get_listed_chromecasts( friendly_names=None, uuids=None, tries=None, retry_wait=None, timeout=None, discovery_timeout=DISCOVER_TIMEOUT, zeroconf_instance=None, known_hosts=None, ): """ Searches the network for chromecast devices matching a list of friendly names or a list of UUIDs. Returns a tuple of: A list of Chromecast objects matching the criteria, or an empty list if no matching chromecasts were found. A service browser to keep the Chromecast mDNS data updated. When updates are (no longer) needed, call browser.stop_discovery(). To only discover chromecast devices without connecting to them, use discover_listed_chromecasts instead. :param friendly_names: A list of wanted friendly names :param uuids: A list of wanted uuids :param tries: passed to get_chromecasts :param retry_wait: passed to get_chromecasts :param timeout: passed to get_chromecasts :param discovery_timeout: A floating point number specifying the time to wait devices matching the criteria have been found. :param zeroconf_instance: An existing zeroconf instance. """ cc_list = {} def add_callback(uuid, _service): _LOGGER.debug( "Found chromecast %s (%s)", browser.devices[uuid].friendly_name, uuid ) def get_chromecast_from_uuid(uuid): return get_chromecast_from_cast_info( browser.devices[uuid], zconf=zconf, tries=tries, retry_wait=retry_wait, timeout=timeout, ) friendly_name = browser.devices[uuid].friendly_name try: if uuids and uuid in uuids: if uuid not in cc_list: cc_list[uuid] = get_chromecast_from_uuid(uuid) uuids.remove(uuid) if friendly_names and friendly_name in friendly_names: if uuid not in cc_list: cc_list[uuid] = get_chromecast_from_uuid(uuid) friendly_names.remove(friendly_name) if not friendly_names and not uuids: discover_complete.set() except ChromecastConnectionError: # noqa: F405 pass discover_complete = Event() zconf = zeroconf_instance or zeroconf.Zeroconf() browser = CastBrowser(SimpleCastListener(add_callback), zconf, known_hosts) browser.start_discovery() # Wait for the timeout or found all wanted devices discover_complete.wait(discovery_timeout) return (list(cc_list.values()), browser) def get_chromecasts( # pylint: disable=too-many-locals tries=None, retry_wait=None, timeout=None, blocking=True, callback=None, zeroconf_instance=None, known_hosts=None, ): """ Searches the network for chromecast devices and creates a Chromecast object for each discovered device. Returns a tuple of: A list of Chromecast objects, or an empty list if no matching chromecasts were found. A service browser to keep the Chromecast mDNS data updated. When updates are (no longer) needed, call browser.stop_discovery(). To only discover chromecast devices without connecting to them, use discover_chromecasts instead. Parameters tries, timeout, retry_wait and blocking_app_launch controls the behavior of the created Chromecast instances. :param tries: Number of retries to perform if the connection fails. None for infinite 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. :param blocking: If True, returns a list of discovered chromecast devices. If False, triggers a callback for each discovered chromecast, and returns a function which can be executed to stop discovery. :param callback: Callback which is triggered for each discovered chromecast when blocking = False. :param zeroconf_instance: An existing zeroconf instance. """ if blocking: # Thread blocking chromecast discovery devices, browser = discover_chromecasts(known_hosts=known_hosts) cc_list = [] for device in devices: try: cc_list.append( get_chromecast_from_cast_info( device, browser.zc, tries=tries, retry_wait=retry_wait, timeout=timeout, ) ) except ChromecastConnectionError: # noqa: F405 pass return (cc_list, browser) # Callback based chromecast discovery if not callable(callback): raise ValueError("Nonblocking discovery requires a callback function.") known_uuids = set() def add_callback(uuid, _service): """Called when zeroconf has discovered a new chromecast.""" if uuid in known_uuids: return try: callback( get_chromecast_from_cast_info( browser.devices[uuid], zconf=zconf, tries=tries, retry_wait=retry_wait, timeout=timeout, ) ) known_uuids.add(uuid) except ChromecastConnectionError: # noqa: F405 pass zconf = zeroconf_instance or zeroconf.Zeroconf() browser = CastBrowser(SimpleCastListener(add_callback), zconf, known_hosts) browser.start_discovery() return browser # pylint: disable=too-many-instance-attributes, too-many-public-methods class Chromecast: """ Class to interface with a ChromeCast. :param host: The host to connect to. :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 infinite 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. :param services: A set of mDNS or host services to try to connect to. If present, parameters host and port are ignored and host and port are instead resolved through mDNS. The list of services may be modified, for example if speaker group leadership is handed over. SocketClient will catch modifications to the list when attempting reconnect. :param zconf: A zeroconf instance, needed if a list of services is passed. The zeroconf instance may be obtained from the browser returned by pychromecast.start_discovery(). """ 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) services = kwargs.pop("services", None) zconf = kwargs.pop("zconf", None) self.logger = logging.getLogger(__name__) # Resolve host to IP address self._services = services self.logger.info("Querying device status") self.device = device if device: dev_status = get_device_status(host, services, zconf) 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), uuid=(device.uuid or dev_status.uuid), cast_type=(device.cast_type or dev_status.cast_type), multizone_supported=( device.multizone_supported or dev_status.multizone_supported ), ) else: self.device = device else: self.device = get_device_status(host, services, zconf) if not self.device: raise ChromecastConnectionError( # noqa: F405 f"Could not connect to {host}:{port or 8009}" ) 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, services=services, zconf=zconf, ) 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 ) @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 ( self.cast_type == CAST_TYPE_CHROMECAST and 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 uri(self): """Returns the device URI (ip:port)""" return f"{self.socket_client.host}:{self.socket_client.port}" @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, force_launch=False): """Start an app on the Chromecast.""" self.logger.info("Starting app %s", app_id) self.socket_client.receiver_controller.launch_app(app_id, force_launch) 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 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(f"volume delta must be greater than zero, not {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(f"volume delta must be greater than zero, not {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 worker thread is not already running, it will be started. 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. """ if not self.socket_client.is_alive(): self.socket_client.start() self.status_event.wait(timeout=timeout) def connect(self): """Connect to the chromecast. Must only be called if the worker thread will not be started. """ self.socket_client.connect() 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 start(self): """ Start the chromecast connection's worker thread. """ self.socket_client.start() def __del__(self): try: self.socket_client.stop.set() except AttributeError: pass def __repr__(self): return ( f"Chromecast({self.socket_client.host!r}, port={self.socket_client.port!r}, " f"device={self.device!r})" ) def __unicode__(self): return ( f"Chromecast({self.socket_client.host}, {self.socket_client.port}, " f"{self.device.friendly_name}, {self.device.model_name}, " f"{self.device.manufacturer})" ) pychromecast-9.4.0/pychromecast/authority_keys_pb2.py000066400000000000000000000075421414324605500231420ustar00rootroot00000000000000# 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-9.4.0/pychromecast/cast_channel_pb2.py000066400000000000000000000451571414324605500225050ustar00rootroot00000000000000# 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-9.4.0/pychromecast/config.py000066400000000000000000000023071414324605500205530ustar00rootroot00000000000000""" Data and methods to retrieve app specific configuration """ import json import requests APP_BACKDROP = "E8C28D3C" APP_YOUTUBE = "233637DE" APP_MEDIA_RECEIVER = "CC1AD845" APP_PLEX = "06ee44ee-e7e3-4249-83b6-f5d0b6f07f34_1" APP_DASHCAST = "84912283" APP_HOMEASSISTANT_LOVELACE = "A078F6B0" APP_HOMEASSISTANT_MEDIA = "B45F4572" APP_SUPLA = "A41B766D" APP_YLEAREENA = "A9BCCB7C" APP_BUBBLEUPNP = "3927FA74" APP_BBCSOUNDS = "03977A48" APP_BBCIPLAYER = "5E81F6DB" 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( f"https://clients3.google.com/cast/chromecast/device/app?a={app_id}" ) return json.loads(req.text[4:]) if req.status_code == 200 else {} except ValueError: # If json fails to parse return {} pychromecast-9.4.0/pychromecast/const.py000066400000000000000000000014411414324605500204320ustar00rootroot00000000000000""" Chromecast constants """ # 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" MF_GOOGLE = "Google Inc." CAST_TYPES = { "chromecast": CAST_TYPE_CHROMECAST, "eureka dongle": CAST_TYPE_CHROMECAST, "chromecast audio": CAST_TYPE_AUDIO, "google home": CAST_TYPE_AUDIO, "google home mini": CAST_TYPE_AUDIO, "google nest mini": CAST_TYPE_AUDIO, "nest audio": CAST_TYPE_AUDIO, "google cast group": CAST_TYPE_GROUP, } # Known models not manufactured by Google CAST_MANUFACTURERS = {} SERVICE_TYPE_HOST = "host" SERVICE_TYPE_MDNS = "mdns" MESSAGE_TYPE = "type" REQUEST_ID = "requestId" SESSION_ID = "sessionId" pychromecast-9.4.0/pychromecast/controllers/000077500000000000000000000000001414324605500213005ustar00rootroot00000000000000pychromecast-9.4.0/pychromecast/controllers/__init__.py000066400000000000000000000100761414324605500234150ustar00rootroot00000000000000""" Provides controllers to handle specific namespaces in Chromecast communication. """ import abc import logging from ..error import UnsupportedNamespace, ControllerNotRegistered class BaseController(abc.ABC): """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, force_launch=False): """If set, launches app related to the controller.""" self._check_registered() self._socket_client.receiver_controller.launch_app( self.supporting_app_id, force_launch=force_launch, 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.""" def channel_disconnected(self): """Called when a channel is disconnected.""" def send_message(self, data, inc_session_id=False, callback_function=None): """ Send a message on this namespace to the Chromecast. Ensures app is loaded. 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( callback_function=lambda: self.send_message_nocheck( data, inc_session_id, callback_function ) ) return raise UnsupportedNamespace( f"Namespace {self.namespace} is not supported by running application." ) self.send_message_nocheck(data, inc_session_id, callback_function) def send_message_nocheck(self, data, inc_session_id=False, callback_function=None): """Send a message.""" self._message_func(self.namespace, data, inc_session_id, callback_function) def receive_message(self, _message, _data: dict): # pylint: disable=no-self-use """ Called when a message is received that matches the namespace. Returns boolean indicating if message was handled. data is message.payload_utf8 interpreted as a JSON dict. """ 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-9.4.0/pychromecast/controllers/bbciplayer.py000066400000000000000000000030321414324605500237640ustar00rootroot00000000000000""" Controller to interface with BBC iPlayer. """ # Note: Media ID is NOT the 8 digit alpha-numeric in the URL # it can be found by right clicking the playing video on the web interface # e.g. https://www.bbc.co.uk/iplayer/episode/b09w7fd9/bitz-bob-series-1-1-castle-makeover shows: # "2908kbps | dash (mf_cloudfront_dash_https) # b09w70r2 | 960x540" import logging from . import BaseController from ..config import APP_BBCIPLAYER from .media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE APP_NAMESPACE = "urn:x-cast:com.google.cast.media" class BbcIplayerController(BaseController): """Controller to interact with BBC iPlayer namespace.""" def __init__(self): super().__init__(APP_NAMESPACE, APP_BBCIPLAYER) self.logger = logging.getLogger(__name__) def play_media(self, media_id, is_live=False, **kwargs): """Play BBC iPlayer media""" stream_type = STREAM_TYPE_LIVE if is_live else STREAM_TYPE_BUFFERED metadata = kwargs.get("metadata", {"metadataType": 0, "title": ""}) subtitle = metadata.pop("subtitle", "") msg = { "media": { "contentId": media_id, "customData": {"secondary_title": subtitle}, "metadata": metadata, "streamType": stream_type, }, "type": "LOAD", } self.send_message(msg, inc_session_id=False) def quick_play(self, media_id=None, is_live=False, **kwargs): """Quick Play""" self.play_media(media_id, is_live=is_live, **kwargs) pychromecast-9.4.0/pychromecast/controllers/bbcsounds.py000066400000000000000000000023441414324605500236370ustar00rootroot00000000000000""" Controller to interface with BBC Sounds. """ # Media ID can be found in the URL # e.g. https://www.bbc.co.uk/sounds/live:bbc_radio_one import logging from . import BaseController from ..config import APP_BBCSOUNDS from .media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE APP_NAMESPACE = "urn:x-cast:com.google.cast.media" class BbcSoundsController(BaseController): """Controller to interact with BBC Sounds namespace.""" def __init__(self): super().__init__(APP_NAMESPACE, APP_BBCSOUNDS) self.logger = logging.getLogger(__name__) def play_media(self, media_id, is_live=False, **kwargs): """Play BBC Sounds media""" stream_type = STREAM_TYPE_LIVE if is_live else STREAM_TYPE_BUFFERED metadata_default = {"metadataType": 0, "title": ""} msg = { "media": { "contentId": media_id, "metadata": kwargs.get("metadata", metadata_default), "streamType": stream_type, }, "type": "LOAD", } self.send_message(msg, inc_session_id=False) def quick_play(self, media_id=None, is_live=False, **kwargs): """Quick Play""" self.play_media(media_id, is_live=is_live, **kwargs) pychromecast-9.4.0/pychromecast/controllers/bubbleupnp.py000066400000000000000000000010401414324605500240030ustar00rootroot00000000000000""" Simple Controller to use BubbleUPNP as a media controller. """ from ..config import APP_BUBBLEUPNP from .media import MediaController class BubbleUPNPController(MediaController): """Controller to interact with BubbleUPNP app namespace.""" def __init__(self): super().__init__() self.app_id = APP_BUBBLEUPNP self.supporting_app_id = APP_BUBBLEUPNP def quick_play(self, media_id=None, media_type="video/mp4", **kwargs): """Quick Play""" self.play_media(media_id, media_type, **kwargs) pychromecast-9.4.0/pychromecast/controllers/dashcast.py000066400000000000000000000035031414324605500234450ustar00rootroot00000000000000""" Controller to interface with the DashCast app namespace. """ from ..config import APP_DASHCAST from . import BaseController APP_NAMESPACE = "urn:x-cast:com.madmod.dashcast" class DashCastController(BaseController): """Controller to interact with DashCast app namespace.""" def __init__(self, appNamespace=APP_NAMESPACE, appId=APP_DASHCAST): super().__init__(appNamespace, appId) def receive_message(self, _message, _data: dict): """ Called when a load complete message is received. This is currently un-used by this controller. It is implemented so that we don't get "Message unhandled" warnings. In the future it might be used to update a public status object like the media controller does. """ # Indicate that the message was successfully handled. return True def load_url(self, url, force=False, reload_seconds=0, callback_function=None): """ Starts loading a URL with an optional reload time in seconds. Setting force to True may load pages which block iframe embedding, but will prevent reload from working and will cause calls to load_url() to reload the app. """ def launch_callback(): """Loads requested URL after app launched.""" should_reload = not force and reload_seconds not in (0, None) reload_milliseconds = 0 if not should_reload else reload_seconds * 1000 msg = { "url": url, "force": force, "reload": should_reload, "reload_time": reload_milliseconds, } self.send_message( msg, inc_session_id=True, callback_function=callback_function ) self.launch(callback_function=launch_callback) pychromecast-9.4.0/pychromecast/controllers/homeassistant.py000066400000000000000000000063321414324605500245400ustar00rootroot00000000000000""" Controller to interface with Home Assistant """ from ..config import APP_HOMEASSISTANT_LOVELACE from . import BaseController APP_NAMESPACE = "urn:x-cast:com.nabucasa.hast" class HomeAssistantController(BaseController): """Controller to interact with Home Assistant.""" def __init__( self, hass_url, client_id, refresh_token, app_namespace=APP_NAMESPACE, app_id=APP_HOMEASSISTANT_LOVELACE, ): super().__init__(app_namespace, app_id) self.hass_url = hass_url self.client_id = client_id self.refresh_token = refresh_token # { # connected: boolean; # showDemo: boolean; # hassUrl?: string; # lovelacePath?: string | number | null; # } self.status = None self._on_connect = [] @property def hass_connected(self): """Return if connected to Home Assistant.""" return ( self.status is not None and self.status["connected"] and self.status["hassUrl"] == self.hass_url ) def channel_connected(self): """Called when a channel has been openend that supports the namespace of this controller.""" self.get_status() def channel_disconnected(self): """Called when a channel is disconnected.""" self.status = None def receive_message(self, _message, data: dict): """Called when a message is received.""" if data.get("type") == "receiver_status": was_connected = self.hass_connected self.status = data if was_connected or not self.hass_connected: return True # We just got connected, call the callbacks. while self._on_connect: self._on_connect.pop()() return True return False def connect_hass(self, callback_function=None): """Connect to Home Assistant.""" self._on_connect.append(callback_function) self.send_message( { "type": "connect", "refreshToken": self.refresh_token, "clientId": self.client_id, "hassUrl": self.hass_url, } ) def show_demo(self): """Show the demo.""" self.send_message({"type": "show_demo"}) def get_status(self, callback_function=None): """Get status of Home Assistant Cast.""" self.send_connected_message( {"type": "get_status"}, callback_function=callback_function ) def show_lovelace_view(self, view_path, url_path=None, callback_function=None): """Show a Lovelace UI.""" self.send_connected_message( {"type": "show_lovelace_view", "viewPath": view_path, "urlPath": url_path}, callback_function=callback_function, ) def send_connected_message(self, data, callback_function=None): """Send a message to a connected Home Assistant Cast""" if self.hass_connected: self.send_message_nocheck(data, callback_function=callback_function) return self.connect_hass( lambda: self.send_message_nocheck(data, callback_function=callback_function) ) pychromecast-9.4.0/pychromecast/controllers/homeassistant_media.py000066400000000000000000000011511414324605500256710ustar00rootroot00000000000000""" Simple Controller to use the Home Assistant Media Player Cast App as a media controller. """ from ..config import APP_HOMEASSISTANT_MEDIA from .media import MediaController class HomeAssistantMediaController(MediaController): """Controller to interact with HomeAssistantMedia app namespace.""" def __init__(self): super().__init__() self.app_id = APP_HOMEASSISTANT_MEDIA self.supporting_app_id = APP_HOMEASSISTANT_MEDIA def quick_play(self, media_id=None, media_type="video/mp4", **kwargs): """Quick Play""" self.play_media(media_id, media_type, **kwargs) pychromecast-9.4.0/pychromecast/controllers/media.py000066400000000000000000000530401414324605500227330ustar00rootroot00000000000000""" Provides a controller for controlling the default media players on the Chromecast. """ import abc from datetime import datetime import logging from collections import namedtuple import threading from ..config import APP_MEDIA_RECEIVER from ..const import MESSAGE_TYPE 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" TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO" TYPE_GET_STATUS = "GET_STATUS" TYPE_LOAD = "LOAD" TYPE_QUEUE_INSERT = "QUEUE_INSERT" TYPE_MEDIA_STATUS = "MEDIA_STATUS" TYPE_PAUSE = "PAUSE" TYPE_PLAY = "PLAY" TYPE_QUEUE_NEXT = "QUEUE_NEXT" TYPE_QUEUE_PREV = "QUEUE_PREV" TYPE_QUEUE_UPDATE = "QUEUE_UPDATE" TYPE_SEEK = "SEEK" TYPE_STOP = "STOP" METADATA_TYPE_GENERIC = 0 METADATA_TYPE_MOVIE = 1 METADATA_TYPE_TVSHOW = 2 METADATA_TYPE_MUSICTRACK = 3 METADATA_TYPE_PHOTO = 4 # From www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js CMD_SUPPORT_PAUSE = 1 CMD_SUPPORT_SEEK = 2 CMD_SUPPORT_STREAM_VOLUME = 4 CMD_SUPPORT_STREAM_MUTE = 8 # ALL_BASIC_MEDIA = PAUSE | SEEK | VOLUME | MUTE | EDIT_TRACKS | PLAYBACK_RATE CMD_SUPPORT_ALL_BASIC_MEDIA = 12303 CMD_SUPPORT_QUEUE_NEXT = 64 CMD_SUPPORT_QUEUE_PREV = 128 CMD_SUPPORT_QUEUE_SHUFFLE = 256 CMD_SUPPORT_QUEUE_REPEAT_ALL = 1024 CMD_SUPPORT_QUEUE_REPEAT_ONE = 2048 CMD_SUPPORT_QUEUE_REPEAT = 3072 CMD_SUPPORT_SKIP_AD = 512 CMD_SUPPORT_EDIT_TRACKS = 4096 CMD_SUPPORT_PLAYBACK_RATE = 8192 CMD_SUPPORT_LIKE = 16384 CMD_SUPPORT_DISLIKE = 32768 CMD_SUPPORT_FOLLOW = 65536 CMD_SUPPORT_UNFOLLOW = 131072 CMD_SUPPORT_STREAM_TRANSFER = 262144 # Legacy? CMD_SUPPORT_SKIP_FORWARD = 16 CMD_SUPPORT_SKIP_BACKWARD = 32 MediaImage = namedtuple("MediaImage", "url height width") _LOGGER = logging.getLogger(__name__) class MediaStatus: """Class to hold the media status.""" 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 = [] self.last_updated = None @property def adjusted_current_time(self): """Returns calculated current seek time of media in seconds""" if self.player_state == MEDIA_PLAYER_STATE_PLAYING: # Add time since last update return ( self.current_time + (datetime.utcnow() - self.last_updated).total_seconds() ) # Not playing, return last reported seek time return self.current_time @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 in ( MEDIA_PLAYER_STATE_PLAYING, 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) @property def supports_queue_next(self): """True if QUEUE_NEXT is supported.""" return bool(self.supported_media_commands & CMD_SUPPORT_QUEUE_NEXT) @property def supports_queue_prev(self): """True if QUEUE_PREV is supported.""" return bool(self.supported_media_commands & CMD_SUPPORT_QUEUE_PREV) def update(self, data): """New data will only contain the changed attributes.""" if not data.get("status", []): 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 ) self.last_updated = datetime.utcnow() 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 f"" class MediaStatusListener(abc.ABC): """Listener for receiving media status events.""" @abc.abstractmethod def new_media_status(self, status: MediaStatus): """Updated media status.""" class MediaController(BaseController): """Controller to interact with Google media namespace.""" def __init__(self): super().__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: dict): """Called when a media message is received.""" if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS: self._process_media_status(data) return True return False def register_status_listener(self, listener: MediaStatusListener): """Register a listener for new media statuses. 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 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 queue_next(self): """Send the QUEUE_NEXT command.""" self._send_command({MESSAGE_TYPE: TYPE_QUEUE_UPDATE, "jump": 1}) def queue_prev(self): """Send the QUEUE_PREV command.""" self._send_command({MESSAGE_TYPE: TYPE_QUEUE_UPDATE, "jump": -1}) 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 _LOGGER.exception("Exception thrown when calling media status callback") def play_media( # pylint: disable=too-many-locals self, url, content_type, title=None, thumb=None, current_time=None, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, metadata=None, subtitles=None, subtitles_lang="en-US", subtitles_mime="text/vtt", subtitle_id=1, enqueue=False, media_info=None, ): """ 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 since beginning of content. If the content is live content, and position is not specifed, the stream will start at the live position 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. enqueue: bool - if True, enqueue the media instead of play it. media_info: dict - additional MediaInformation attributes not explicitly listed. metadata: dict - media metadata object, one of the following: GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata, MusicTrackMediaMetadata, PhotoMediaMetadata. Docs: https://developers.google.com/cast/docs/reference/messages#MediaData https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation """ 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, enqueue, media_info, ) receiver_ctrl = self._socket_client.receiver_controller receiver_ctrl.launch_app(self.app_id, callback_function=app_launched_callback) def _send_start_play_media( # pylint: disable=too-many-locals self, url, content_type, title=None, thumb=None, current_time=None, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, metadata=None, subtitles=None, subtitles_lang="en-US", subtitles_mime="text/vtt", subtitle_id=1, enqueue=False, media_info=None, ): media_info = media_info or {} media = { "contentId": url, "streamType": stream_type, "contentType": content_type, "metadata": metadata or {}, **media_info, } if title: media["metadata"]["title"] = title if thumb: media["metadata"]["thumb"] = thumb if "images" not in media["metadata"]: media["metadata"]["images"] = [] media["metadata"]["images"].append({"url": thumb}) # Need to set metadataType if not specified # https://developers.google.com/cast/docs/reference/messages#MediaInformation if media["metadata"] and "metadataType" not in media["metadata"]: media["metadata"]["metadataType"] = METADATA_TYPE_GENERIC if subtitles: sub_msg = [ { "trackId": subtitle_id, "trackContentId": subtitles, "language": subtitles_lang, "subtype": "SUBTITLES", "type": "TEXT", "trackContentType": subtitles_mime, "name": f"{subtitles_lang} - {subtitle_id} Subtitle", } ] media["tracks"] = sub_msg media["textTrackStyle"] = { "backgroundColor": "#FFFFFF00", "edgeType": "OUTLINE", "edgeColor": "#000000FF", } if enqueue: msg = { "mediaSessionId": self.status.media_session_id, "items": [ { "media": media, "autoplay": True, "startTime": 0, "preloadTime": 0, } ], MESSAGE_TYPE: TYPE_QUEUE_INSERT, } else: msg = { "media": media, MESSAGE_TYPE: TYPE_LOAD, } if current_time is not None: msg["currentTime"] = current_time msg["autoplay"] = autoplay msg["customData"] = {} if subtitles: msg["activeTrackIds"] = [subtitle_id] self.send_message(msg, inc_session_id=True) def tear_down(self): """Called when controller is destroyed.""" super().tear_down() self._status_listeners[:] = [] pychromecast-9.4.0/pychromecast/controllers/multizone.py000066400000000000000000000257671414324605500237210ustar00rootroot00000000000000""" Controller to monitor audio group members. """ import abc import logging from . import BaseController from .media import MediaStatus from .receiver import CastStatus from ..const import MESSAGE_TYPE from ..socket_client import ( CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_LOST, ) _LOGGER = logging.getLogger(__name__) MULTIZONE_NAMESPACE = "urn:x-cast:com.google.cast.multizone" TYPE_CASTING_GROUPS = "CASTING_GROUPS" TYPE_DEVICE_ADDED = "DEVICE_ADDED" TYPE_DEVICE_UPDATED = "DEVICE_UPDATED" TYPE_DEVICE_REMOVED = "DEVICE_REMOVED" TYPE_GET_CASTING_GROUPS = "GET_CASTING_GROUPS" TYPE_GET_STATUS = "GET_STATUS" TYPE_MULTIZONE_STATUS = "MULTIZONE_STATUS" TYPE_SESSION_UPDATED = "PLAYBACK_SESSION_UPDATED" class Listener: """Callback handler.""" def __init__(self, group_cast, casts): """Initialize the listener.""" self._casts = casts group_cast.register_status_listener(self) group_cast.media_controller.register_status_listener(self) group_cast.register_connection_listener(self) self._mz = MultizoneController(group_cast.uuid) self._mz.register_listener(self) self._group_uuid = str(group_cast.uuid) group_cast.register_handler(self._mz) def new_cast_status(self, cast_status): """Handle reception of a new CastStatus.""" casts = self._casts group_members = self._mz.members for member_uuid in group_members: if member_uuid not in casts: continue for listener in list(casts[member_uuid]["listeners"]): listener.multizone_new_cast_status(self._group_uuid, cast_status) def new_media_status(self, media_status): """Handle reception of a new MediaStatus.""" casts = self._casts group_members = self._mz.members for member_uuid in group_members: if member_uuid not in casts: continue for listener in list(casts[member_uuid]["listeners"]): listener.multizone_new_media_status(self._group_uuid, media_status) def new_connection_status(self, conn_status): """Handle reception of a new ConnectionStatus.""" if conn_status.status == CONNECTION_STATUS_CONNECTED: self._mz.update_members() if conn_status.status in ( CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_LOST, ): self._mz.reset_members() def multizone_member_added(self, member_uuid): """Handle added audio group member.""" casts = self._casts if member_uuid not in casts: casts[member_uuid] = {"listeners": [], "groups": set()} casts[member_uuid]["groups"].add(self._group_uuid) for listener in list(casts[member_uuid]["listeners"]): listener.added_to_multizone(self._group_uuid) def multizone_member_removed(self, member_uuid): """Handle removed audio group member.""" casts = self._casts if member_uuid not in casts: casts[member_uuid] = {"listeners": [], "groups": set()} casts[member_uuid]["groups"].discard(self._group_uuid) for listener in list(casts[member_uuid]["listeners"]): listener.removed_from_multizone(self._group_uuid) def multizone_status_received(self): """Handle reception of audio group status.""" class MultiZoneManagerListener(abc.ABC): """Listener for receiving audio group events for a cast device.""" @abc.abstractmethod def added_to_multizone(self, group_uuid: str): """The cast has been added to group identified by group_uuid.""" @abc.abstractmethod def removed_from_multizone(self, group_uuid: str): """The cast has been removed from group identified by group_uuid.""" @abc.abstractmethod def multizone_new_media_status(self, group_uuid: str, media_status: MediaStatus): """The group identified by group_uuid, of which the cast is a member, has new media status.""" @abc.abstractmethod def multizone_new_cast_status(self, group_uuid: str, cast_status: CastStatus): """The group identified by group_uuid, of which the cast is a member, has new status.""" class MultizoneManager: """Manage audio groups.""" def __init__(self): # Protect self._casts because it will be accessed from callbacks from # the casts' socket_client thread self._casts = {} self._groups = {} def add_multizone(self, group_cast): """Start managing a group""" self._groups[str(group_cast.uuid)] = { "chromecast": group_cast, "listener": Listener(group_cast, self._casts), "members": set(), } def remove_multizone(self, group_uuid): """Stop managing a group""" group_uuid = str(group_uuid) group = self._groups.pop(group_uuid, None) # Inform all group members that they are no longer members if group is not None: group["listener"]._mz.reset_members() # pylint: disable=protected-access for member in self._casts.values(): member["groups"].discard(group_uuid) def register_listener(self, member_uuid, listener: MultiZoneManagerListener): """Register a listener for audio group changes of cast uuid. On update will call: listener.added_to_multizone(group_uuid) The cast has been added to group uuid listener.removed_from_multizone(group_uuid) The cast has been removed from group uuid listener.multizone_new_media_status(group_uuid, media_status) The group uuid, of which the cast is a member, has new status listener.multizone_new_cast_status(group_uuid, cast_status) The group uuid, of which the cast is a member, has new status """ member_uuid = str(member_uuid) if member_uuid not in self._casts: self._casts[member_uuid] = {"listeners": [], "groups": set()} self._casts[member_uuid]["listeners"].append(listener) def deregister_listener(self, member_uuid, listener): """Deregister listener for audio group changes of cast uuid.""" self._casts[str(member_uuid)]["listeners"].remove(listener) def get_multizone_memberships(self, member_uuid): """Return a list of audio groups in which cast member_uuid is a member""" return list(self._casts[str(member_uuid)]["groups"]) def get_multizone_mediacontroller(self, group_uuid): """Get mediacontroller of a group""" return self._groups[str(group_uuid)]["chromecast"].media_controller class MultiZoneControllerListener(abc.ABC): """Listener for receiving audio group events.""" @abc.abstractmethod def multizone_member_added(self, group_uuid: str): """The cast has been added to group identified by group_uuid.""" @abc.abstractmethod def multizone_member_removed(self, group_uuid: str): """The cast has been removed from group identified by group_uuid.""" @abc.abstractmethod def multizone_status_received(self): """Multizone status has been updated.""" class MultizoneController(BaseController): """Controller to monitor audio group members.""" def __init__(self, uuid): self._members = {} self._status_listeners = [] self._uuid = str(uuid) super().__init__(MULTIZONE_NAMESPACE, target_platform=True) def _add_member(self, uuid, name): if uuid not in self._members: self._members[uuid] = name _LOGGER.debug( "(%s) Added member %s(%s), members: %s", self._uuid, uuid, name, self._members, ) for listener in list(self._status_listeners): listener.multizone_member_added(uuid) def _remove_member(self, uuid): name = self._members.pop(uuid, "") _LOGGER.debug( "(%s) Removed member %s(%s), members: %s", self._uuid, uuid, name, self._members, ) for listener in list(self._status_listeners): listener.multizone_member_removed(uuid) def register_listener(self, listener: MultiZoneControllerListener): """Register a listener for audio group changes. On update will call: listener.multizone_member_added(uuid) listener.multizone_member_removed(uuid) listener.multizone_status_received() """ self._status_listeners.append(listener) @property def members(self): """Return a list of audio group members.""" return list(self._members.keys()) def reset_members(self): """Reset audio group members.""" for uuid in list(self._members): self._remove_member(uuid) def update_members(self): """Update audio group members.""" self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS}) def get_casting_groups(self): """Send GET_CASTING_GROUPS message.""" self.send_message({MESSAGE_TYPE: TYPE_GET_CASTING_GROUPS}) def receive_message( self, _message, data: dict ): # pylint: disable=too-many-return-statements """Called when a multizone message is received.""" if data[MESSAGE_TYPE] == TYPE_DEVICE_ADDED: uuid = data["device"]["deviceId"] name = data["device"]["name"] self._add_member(uuid, name) return True if data[MESSAGE_TYPE] == TYPE_DEVICE_REMOVED: uuid = data["deviceId"] self._remove_member(uuid) return True if data[MESSAGE_TYPE] == TYPE_DEVICE_UPDATED: uuid = data["device"]["deviceId"] name = data["device"]["name"] self._add_member(uuid, name) return True if data[MESSAGE_TYPE] == TYPE_MULTIZONE_STATUS: members = data["status"]["devices"] members = {member["deviceId"]: member["name"] for member in members} removed_members = list(set(self._members.keys()) - set(members.keys())) added_members = list(set(members.keys()) - set(self._members.keys())) _LOGGER.debug( "(%s) Added members %s, Removed members: %s", self._uuid, added_members, removed_members, ) for uuid in removed_members: self._remove_member(uuid) for uuid in added_members: self._add_member(uuid, members[uuid]) for listener in list(self._status_listeners): listener.multizone_status_received() return True if data[MESSAGE_TYPE] == TYPE_SESSION_UPDATED: # A temporary group has been formed return True if data[MESSAGE_TYPE] == TYPE_CASTING_GROUPS: # Answer to GET_CASTING_GROUPS return True return False def tear_down(self): """Called when controller is destroyed.""" super().tear_down() self._status_listeners[:] = [] pychromecast-9.4.0/pychromecast/controllers/plex.py000066400000000000000000000434131414324605500226270ustar00rootroot00000000000000""" Controller to interface with the Plex-app. """ import json import threading from copy import deepcopy from urllib.parse import urlparse from . import BaseController from ..const import MESSAGE_TYPE STREAM_TYPE_UNKNOWN = "UNKNOWN" STREAM_TYPE_BUFFERED = "BUFFERED" STREAM_TYPE_LIVE = "LIVE" SEEK_KEY = "currentTime" TYPE_PLAY = "PLAY" TYPE_PAUSE = "PAUSE" TYPE_STOP = "STOP" TYPE_STEPFORWARD = "STEPFORWARD" TYPE_STEPBACKWARD = "STEPBACK" TYPE_PREVIOUS = "PREVIOUS" TYPE_NEXT = "NEXT" TYPE_LOAD = "LOAD" TYPE_DETAILS = "SHOWDETAILS" TYPE_SEEK = "SEEK" TYPE_MEDIA_STATUS = "MEDIA_STATUS" TYPE_GET_STATUS = "GET_STATUS" TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO" def media_to_chromecast_command( media=None, type="LOAD", # pylint: disable=redefined-builtin requestId=1, offset=0, directPlay=True, directStream=True, subtitleSize=100, audioBoost=100, transcoderVideo=True, transcoderVideoRemuxOnly=False, transcoderAudio=True, isVerifiedHostname=True, contentType="video", myPlexSubscription=True, contentId=None, streamType=STREAM_TYPE_BUFFERED, port=32400, protocol="http", address=None, username=None, autoplay=True, currentTime=0, playQueue=None, playQueueID=None, startItem=None, version="1.10.1.4602", **kwargs, ): # pylint: disable=invalid-name, too-many-locals, protected-access """Create the message that chromecast requires. Use pass of plexapi media object or set all the needed kwargs manually. See the code for what to set. Args: media (None, optional): a :class:`~plexapi.base.Playable type (str): Default LOAD, SHOWDETAILS. requestId (int): The requestId, Chromecasts may use this. offset (int): Offset of the playback in seconds. directPlay (bool): Default True directStream (bool): Default True subtitleSize (int): Set the subtitle size, possibly only 100 & 200. audioBoost (int): Default 100 transcoderVideo (bool): Default True transcoderVideoRemuxOnly (bool): Default False transcoderAudio (bool): Default True isVerifiedHostname (bool): Default True contentType (str): Default 'video', 'audio' myPlexSubscription (bool): True if user has a PlexPass. contentId (str): The key Chromecasts use to start playback. streamType (str): Default BUFFERED, LIVE port (int): PMS port address (str): PMS host, without scheme. username (None): Username of the user that started playback. autoplay (bool): Auto play after the video is done. currentTime (int): Set playback from this time. default 0 version (str): PMS version. Default 1.10.1.4602 startItem (:class:`~plexapi.media.Media`, optional): Media item in list/playlist/playqueue where playback should start. Overrides existing startItem for playqueues if set. **kwargs: To allow overrides, this will be merged with the rest of the msg. Returns: dict: Returs a dict formatted correctly to start playback on a Chromecast. """ if media is not None: # Lets set some params for the user if they use plexapi. server = media[0]._server if isinstance(media, list) else media._server server_url = urlparse(server._baseurl) protocol = server_url.scheme address = server_url.hostname port = server_url.port machineIdentifier = server.machineIdentifier token = server._token username = server.myPlexUsername myPlexSubscription = server.myPlexSubscription if getattr(media, "TYPE", None) == "playqueue": if startItem: media = media.items else: playQueue = media if playQueue is None: playQueue = server.createPlayQueue(media, startItem=startItem) playQueueID = playQueue.playQueueID contentId = playQueue.selectedItem.key contentType = playQueue.items[0].listType version = server.version # Chromecasts seem to start playback 5 seconds before the offset. if offset != 0: currentTime = offset msg = { "type": type, "requestId": requestId, "media": { "contentId": contentId, "streamType": streamType, "contentType": contentType, "customData": { "offset": offset, "directPlay": directPlay, "directStream": directStream, "subtitleSize": subtitleSize, "audioBoost": audioBoost, "server": { "machineIdentifier": machineIdentifier, "transcoderVideo": transcoderVideo, "transcoderVideoRemuxOnly": transcoderVideoRemuxOnly, "transcoderAudio": transcoderAudio, "version": version, "myPlexSubscription": myPlexSubscription, "isVerifiedHostname": isVerifiedHostname, "protocol": protocol, "address": address, "port": port, "accessToken": token, "user": {"username": username}, }, "containerKey": f"/playQueues/{playQueueID}?own=1&window=200", }, "autoplay": autoplay, "currentTime": currentTime, "activeTrackIds": None, }, } # Allow passing of kwargs to the dict. msg.update(kwargs) return msg @property def episode_title(self): """Return episode title.""" return self.media_metadata.get("subtitle") class PlexController(BaseController): # pylint: disable=too-many-public-methods """Controller to interact with Plex namespace.""" def __init__(self): super().__init__("urn:x-cast:plex", "9AC194DC") self.app_id = "9AC194DC" self.namespace = "urn:x-cast:plex" self.request_id = 0 self.play_media_event = threading.Event() self._last_play_msg = {} def _send_cmd( self, msg, namespace=None, inc_session_id=False, callback_function=None, inc=True, ): # pylint: disable=too-many-arguments """Wrapper for the commands. Args: msg (dict): The actual command that will be sent. namespace (None, optional): What namespace should be used to send this. inc_session_id (bool, optional): Include session ID. callback_function (None, optional): If callback is provided it is executed after the command. inc (bool, optional): Increase the requestsId. """ self.logger.debug( "Sending msg %r %s %s %s %s.", msg, namespace, inc_session_id, callback_function, inc, ) if inc: self._inc_request() if namespace: old = self.namespace try: self.namespace = namespace self.send_message( msg, inc_session_id=inc_session_id, callback_function=callback_function, ) finally: self.namespace = old else: self.send_message( msg, inc_session_id=inc_session_id, callback_function=callback_function ) def _inc_request(self): # Is this getting passed to Plex? self.request_id += 1 return self.request_id def channel_connected(self): """Updates status when a media channel is connected.""" self.update_status() def receive_message(self, _message, data: dict): """Called when a message from Plex to our controller is received. I haven't seen any message for it, but lets keep for for now. I have done minimal testing. Args: message (dict): Description data (dict): message.payload_utf8 interpreted as a JSON dict. Returns: bool: True if the message is handled. """ if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS: self.logger.debug("(PlexController) MESSAGE RECEIVED: %r.", data) return True return False def update_status(self, callback_function_param=False): """Send message to update status.""" self.send_message( {MESSAGE_TYPE: TYPE_GET_STATUS}, callback_function=callback_function_param ) def stop(self): """Send stop command.""" self._send_cmd({MESSAGE_TYPE: TYPE_STOP}) def pause(self): """Send pause command.""" self._send_cmd({MESSAGE_TYPE: TYPE_PAUSE}) def play(self): """Send play command.""" self._send_cmd({MESSAGE_TYPE: TYPE_PLAY}) def previous(self): """Send previous command.""" self._send_cmd({MESSAGE_TYPE: TYPE_PREVIOUS}) def next(self): """Send next command.""" self._send_cmd({MESSAGE_TYPE: TYPE_NEXT}) def seek(self, position, resume_state="PLAYBACK_START"): """Send seek command. Args: position (int): Offset in seconds. resume_state (str, default): PLAYBACK_START """ self._send_cmd( {MESSAGE_TYPE: TYPE_SEEK, SEEK_KEY: position, "resumeState": resume_state} ) def rewind(self): """Rewind back to the start.""" self.seek(0) def set_volume(self, percent): """Set the volume in percent (1-100). Args: percent (int): Percent of volume to be set. """ self._socket_client.receiver_controller.set_volume(float(percent / 100)) def volume_up(self, delta=0.1): """Increment volume by 0.1 (or delta) unless at max. Returns the new volume. """ if delta <= 0: raise ValueError(f"volume delta must be greater than zero, not {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 at 0. Returns the new volume. """ if delta <= 0: raise ValueError(f"volume delta must be greater than zero, not {delta}") return self.set_volume(self.status.volume_level - delta) def mute(self, status=None): """Toggle muting of audio. Args: status (None, optional): Override for on/off. """ if status is None: status = not self.status.volume_muted self._socket_client.receiver_controller.set_volume_muted(status) def show_media(self, media=None, **kwargs): """Show media item's info on screen.""" msg = media_to_chromecast_command( media, type=TYPE_DETAILS, requestId=self._inc_request(), **kwargs ) def callback(): # pylint: disable=missing-docstring self._send_cmd(msg, inc_session_id=True, inc=False) self.launch(callback) def quit_app(self): """Quit the Plex app.""" self._socket_client.receiver_controller.stop_app() @property def status(self): """Get the Chromecast's playing status. Returns: pychromecast.controllers.media.MediaStatus: Slightly modified status with patched method for episode_title. """ status = self._socket_client.media_controller.status status.episode_title = episode_title return status def _reset_playback(self, offset=None): """Reset playback. Args: offset (None, optional): Start playback from this offset in seconds, otherwise playback will start from current time. """ if self._last_play_msg: offset_now = self.status.adjusted_current_time msg = deepcopy(self._last_play_msg) msg["media"]["customData"]["offset"] = ( offset_now if offset is None else offset ) msg["current_time"] = offset_now self._send_cmd( msg, namespace="urn:x-cast:com.google.cast.media", inc_session_id=True, inc=False, ) else: self.logger.debug( "Can not reset the stream, _last_play_msg " "was not set with _send_start_play." ) def _send_start_play(self, media=None, **kwargs): """Helper to send a playback command. Args: media (None, optional): :class:`~plexapi.base.Playable **kwargs: media_to_chromecast_command docs string. """ msg = media_to_chromecast_command( media, requestiId=self._inc_request(), **kwargs ) self.logger.debug("Create command: \n%r\n", json.dumps(msg, indent=4)) self._last_play_msg = msg self._send_cmd( msg, namespace="urn:x-cast:com.google.cast.media", inc_session_id=True, inc=False, ) def block_until_playing(self, media=None, timeout=None, **kwargs): """Block until media is playing, typically useful in a script. Another way to do the same is to check if the controller is_active or by using self.status.player_state. Args: media (None, optional): Can also be :class:`~plexapi.base.Playable if not, you need to fill out all the kwargs. timeout (None, int): default None **kwargs: See media_to_chromecast_command docs string. """ # In case media isnt playing. self.play_media_event.clear() self.play_media(media, **kwargs) self.play_media_event.wait(timeout) self.play_media_event.clear() def play_media(self, media=None, **kwargs): """Start playback on the Chromecast. Args: media (None, optional): Can also be :class:`~plexapi.base.Playable if not, you need to fill out all the kwargs. **kwargs: See media_to_chromecast_command docs string. """ self.play_media_event.clear() def app_launched_callback(): # pylint: disable=missing-docstring try: self._send_start_play(media, **kwargs) finally: self.play_media_event.set() self.launch(app_launched_callback) def join(self, timeout=None): """Join the thread.""" self._socket_client.join(timeout=timeout) def disconnect(self, timeout=None, blocking=True): """Disconnect the controller.""" self._socket_client.disconnect() if blocking: self.join(timeout=timeout) # pylint: disable=too-many-public-methods class PlexApiController(PlexController): """A controller that can use PlexAPI.""" def __init__(self, pms): super().__init__() self.pms = pms def _get_current_media(self): """Get current media_item, media, & part for PMS.""" key = int(self.status.content_id.split("/")[-1]) media_item = self.pms.fetchItem(key).reload() media_idx = self.status.media_custom_data.get("mediaIndex", 0) part_idx = self.status.media_custom_data.get("partIndex", 0) media = media_item.media[media_idx] part = media.parts[part_idx] return media_item, media, part def _change_track(self, track, type_="subtitle", reset_playback=True): """Sets a new default audio/subtitle track. Args: track (None): The chosen track. type_ (str): The type of track. reset_playback (bool, optional): Reset playback after the track has been changed. Raises: ValueError: If type isn't subtitle or audio. """ item, _, part = self._get_current_media() if type_ == "subtitle": method = part.subtitleStreams() default = part.setDefaultSubtitleStream elif type_ == "audio": method = part.audioStreams() default = part.setDefaultAudioStream else: raise ValueError("Set type parameter as subtitle or audio.") for track_ in method: if track in (track_.index, track_.language, track_.languageCode): self.logger.debug("Change %s to %s.", type_, track) default(track_) break item.reload() if reset_playback: self._reset_playback() def enable_audiotrack(self, audio): """Enable an audiotrack. Args: audio (str): Can be index, language or languageCode. """ self._change_track(self, audio, "audio") def disable_subtitle(self): """Disable a subtitle track.""" ( _, __, part, ) = self._get_current_media() part.resetDefaultSubtitleStream() self._reset_playback() def enable_subtitle(self, subtitle): """Enable a subtitle track. Args: subtitle (str): Can be index, language or languageCode. """ self._change_track(subtitle) def play_media(self, media=None, **kwargs): """Start playback on the Chromecast. Args: media (None, optional): Can also be :class:`~plexapi.base.Playable if not, you need to fill out all the kwargs. **kwargs: See media_to_chromecast_command docs string. `version` is set to the version of the PMS reported by the API by default. """ args = {"version": self.pms.version} args.update(kwargs) super().play_media(media, **args) pychromecast-9.4.0/pychromecast/controllers/receiver.py000066400000000000000000000221221414324605500234550ustar00rootroot00000000000000""" Provides a controller for controlling the default media players on the Chromecast. """ import abc from collections import namedtuple from ..const import ( CAST_TYPE_AUDIO, CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP, MESSAGE_TYPE, REQUEST_ID, SESSION_ID, ) from . import BaseController APP_ID = "appId" ERROR_REASON = "reason" NS_RECEIVER = "urn:x-cast:com.google.cast.receiver" TYPE_GET_STATUS = "GET_STATUS" TYPE_RECEIVER_STATUS = "RECEIVER_STATUS" TYPE_LAUNCH = "LAUNCH" TYPE_LAUNCH_ERROR = "LAUNCH_ERROR" VOLUME_CONTROL_TYPE_ATTENUATION = "attenuation" VOLUME_CONTROL_TYPE_FIXED = "fixed" VOLUME_CONTROL_TYPE_MASTER = "master" CastStatus = namedtuple( "CastStatus", [ "is_active_input", "is_stand_by", "volume_level", "volume_muted", "app_id", "display_name", "namespaces", "session_id", "transport_id", "status_text", "icon_url", "volume_control_type", ], ) LaunchFailure = namedtuple("LaunchStatus", ["reason", "app_id", "request_id"]) class CastStatusListener(abc.ABC): """Listener for receiving cast status events.""" @abc.abstractmethod def new_cast_status(self, status: CastStatus): """Updated cast status.""" class LaunchErrorListener(abc.ABC): """Listener for receiving launch error events.""" @abc.abstractmethod def new_launch_error(self, status: LaunchFailure): """Launch error.""" 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): super().__init__(NS_RECEIVER, target_platform=True) self.status = None self.launch_failure = None self.app_to_launch = None self.cast_type = cast_type self.app_launch_event_function = None self._status_listeners = [] self._launch_error_listeners = [] def disconnected(self): """Called when disconnected. Will erase status.""" self.logger.info("Receiver:channel_disconnected") self.status = None @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: dict): """ Called when a receiver message is received. data is message.payload_utf8 interpreted as a JSON dict. """ if data[MESSAGE_TYPE] == TYPE_RECEIVER_STATUS: self._process_get_status(data) return True if data[MESSAGE_TYPE] == TYPE_LAUNCH_ERROR: self._process_launch_error(data) return True return False def register_status_listener(self, listener: CastStatusListener): """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: LaunchErrorListener): """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.status 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_function = callback_function self.launch_failure = None self.send_message({MESSAGE_TYPE: TYPE_LAUNCH, APP_ID: app_id}) else: self.logger.info("Not launching app %s - already running", app_id) if callback_function: callback_function() 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, IndexError): 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", ""), app_data.get("iconUrl"), volume_data.get("controlType", VOLUME_CONTROL_TYPE_ATTENUATION), ) 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 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 self.logger.exception( "Exception thrown when calling cast status listener" ) @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.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 self.logger.exception( "Exception thrown when calling launch error listener" ) def tear_down(self): """Called when controller is destroyed.""" super().tear_down() self.status = None self.launch_failure = None self.app_to_launch = None self._status_listeners[:] = [] pychromecast-9.4.0/pychromecast/controllers/supla.py000066400000000000000000000022731414324605500230020ustar00rootroot00000000000000""" Controller to interface with Supla. """ import logging from . import BaseController from ..config import APP_SUPLA APP_NAMESPACE = "urn:x-cast:fi.ruutu.chromecast" # pylint: disable=too-many-instance-attributes class SuplaController(BaseController): """Controller to interact with Supla namespace.""" def __init__(self): super().__init__(APP_NAMESPACE, APP_SUPLA) self.logger = logging.getLogger(__name__) def play_media(self, media_id, is_live=False): """ Play Supla media """ msg = { "type": "load", "mediaId": media_id, "currentTime": 0, "isLive": is_live, "isAtLiveMoment": False, "bookToken": "", "sample": True, "fw_site": "Supla", "Sanoma_adkv": "", "prerollAdsPlayed": True, "supla": True, "nextInSequenceList": 0, "playbackRate": 1, "env": "prod", } self.send_message(msg, inc_session_id=True) def quick_play(self, media_id=None, is_live=False, **kwargs): """Quick Play""" self.play_media(media_id, is_live=is_live, **kwargs) pychromecast-9.4.0/pychromecast/controllers/yleareena.py000066400000000000000000000036671414324605500236330ustar00rootroot00000000000000""" Controller to interface with the Yle Areena app namespace. """ from ..config import APP_YLEAREENA from .media import MediaController, STREAM_TYPE_BUFFERED, TYPE_LOAD, MESSAGE_TYPE class YleAreenaController(MediaController): """Controller to interact with Yle Areena app namespace.""" def __init__(self): super().__init__() self.app_id = APP_YLEAREENA self.supporting_app_id = APP_YLEAREENA def play_areena_media( # pylint: disable=too-many-locals self, kaltura_id, audio_language="", text_language="off", current_time=0, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, ): """ Play media with the entry id "kaltura_id". This value can be found by loading a page on Areena, e.g. https://areena.yle.fi/1-50097921 And finding the kaltura player which has an id of yle-kaltura-player3430579305188-29-0_whwjqpry In this case the kaltura id is 0_whwjqpry """ msg = { "media": { "streamType": stream_type, "customData": { "mediaInfo": {"entryId": kaltura_id}, "audioLanguage": audio_language, "textLanguage": text_language, }, }, MESSAGE_TYPE: TYPE_LOAD, "currentTime": current_time, "autoplay": autoplay, "customData": {}, "textTrackStyle": { "foregroundColor": "#FFFFFFFF", "backgroundColor": "#000000FF", "fontScale": 1, "fontFamily": "sans-serif", }, } self.send_message(msg, inc_session_id=True) def quick_play(self, media_id=None, audio_lang="", text_lang="off", **kwargs): """Quick Play""" self.play_areena_media( media_id, audio_language=audio_lang, text_language=text_lang, **kwargs ) pychromecast-9.4.0/pychromecast/controllers/youtube.py000066400000000000000000000074311414324605500233530ustar00rootroot00000000000000""" Controller to interface with the YouTube-app. Use the media controller to play, pause etc. """ import logging import threading from casttube import YouTubeSession from . import BaseController from ..const import MESSAGE_TYPE from ..error import UnsupportedNamespace from ..config import APP_YOUTUBE YOUTUBE_NAMESPACE = "urn:x-cast:com.google.youtube.mdx" TYPE_GET_SCREEN_ID = "getMdxSessionStatus" TYPE_STATUS = "mdxSessionStatus" ATTR_SCREEN_ID = "screenId" _LOGGER = logging.getLogger(__name__) class YouTubeController(BaseController): """Controller to interact with Youtube.""" def __init__(self): super().__init__(YOUTUBE_NAMESPACE, APP_YOUTUBE) self.status_update_event = threading.Event() self._screen_id = None self._session = None def start_session_if_none(self): """ Starts a session it is not yet initialized. """ if not (self._screen_id and self._session): self.update_screen_id() self._session = YouTubeSession(screen_id=self._screen_id) def play_video(self, video_id, playlist_id=None): """ Play video(video_id) now. This ignores the current play queue order. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) :param playlist_id: youtube.com/watch?v=video_id&list=playlist_id """ self.start_session_if_none() self._session.play_video(video_id, playlist_id) def add_to_queue(self, video_id): """ Add video(video_id) to the end of the play queue. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) """ self.start_session_if_none() self._session.add_to_queue(video_id) def play_next(self, video_id): """ Play video(video_id) after the currently playing video. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) """ self.start_session_if_none() self._session.play_next(video_id) def remove_video(self, video_id): """ Remove video(videoId) from the queue. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) """ self.start_session_if_none() self._session.remove_video(video_id) def clear_playlist(self): """ Clear the entire video queue """ self.start_session_if_none() self._session.clear_playlist() def update_screen_id(self): """ Sends a getMdxSessionStatus to get the screenId and waits for response. This function is blocking If connected we should always get a response (send message will launch app if it is not running). """ self.status_update_event.clear() # This gets the screenId but always throws. Couldn't find a better way. try: self.send_message({MESSAGE_TYPE: TYPE_GET_SCREEN_ID}) except UnsupportedNamespace: pass status = self.status_update_event.wait(10) if not status: _LOGGER.warning("Failed to update screen_id") self.status_update_event.clear() def receive_message(self, _message, data: dict): """Called when a message is received.""" if data[MESSAGE_TYPE] == TYPE_STATUS: self._process_status(data.get("data")) return True return False def _process_status(self, status): """Process latest status update.""" self._screen_id = status.get(ATTR_SCREEN_ID) self.status_update_event.set() def quick_play(self, media_id=None, playlist_id=None, enqueue=False, **kwargs): """Quick Play""" if enqueue: self.add_to_queue(media_id, **kwargs) else: self.play_video(media_id, playlist_id=playlist_id, **kwargs) pychromecast-9.4.0/pychromecast/dial.py000066400000000000000000000153741414324605500202270ustar00rootroot00000000000000""" Implements the DIAL-protocol to communicate with the Chromecast """ from collections import namedtuple import json import logging import socket import ssl import urllib.request from uuid import UUID import zeroconf from .const import ( CAST_TYPE_AUDIO, CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP, SERVICE_TYPE_HOST, ) XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}" FORMAT_BASE_URL_HTTP = "http://{}:8008" FORMAT_BASE_URL_HTTPS = "https://{}:8443" _LOGGER = logging.getLogger(__name__) def get_host_from_service(service, zconf): """Resolve host and port from service.""" service_info = None if service.type == SERVICE_TYPE_HOST: return service.data + (None,) try: service_info = zconf.get_service_info("_googlecast._tcp.local.", service.data) if service_info: _LOGGER.debug( "get_info_from_service resolved service %s to service_info %s", service, service_info, ) except IOError: pass return _get_host_from_zc_service_info(service_info) + (service_info,) def _get_host_from_zc_service_info(service_info: zeroconf.ServiceInfo): """Get hostname or IP + port from zeroconf service_info.""" host = None port = None if ( service_info and service_info.port and (service_info.server or len(service_info.addresses) > 0) ): if len(service_info.addresses) > 0: host = socket.inet_ntoa(service_info.addresses[0]) else: host = service_info.server.lower() port = service_info.port return (host, port) def _get_status(host, services, zconf, path, secure, timeout, context): """ :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 """ if not host: for service in services.copy(): host, _, _ = get_host_from_service(service, zconf) if host: _LOGGER.debug("Resolved service %s to %s", service, host) break headers = {"content-type": "application/json"} if secure: url = FORMAT_BASE_URL_HTTPS.format(host) + path else: url = FORMAT_BASE_URL_HTTP.format(host) + path has_context = bool(context) if secure and not has_context: context = get_ssl_context() req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=timeout, context=context) as response: data = response.read() return json.loads(data.decode("utf-8")) def get_ssl_context(): """Create an SSL context.""" context = ssl.SSLContext() context.verify_mode = ssl.CERT_NONE return context def get_device_status( # pylint: disable=too-many-locals host, services=None, zconf=None, timeout=30, context=None ): """ :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: status = _get_status( host, services, zconf, "/setup/eureka_info?params=device_info,name", True, timeout, context, ) cast_type = CAST_TYPE_CHROMECAST display_supported = True friendly_name = status.get("name", "Unknown Chromecast") manufacturer = "Unknown manufacturer" model_name = "Unknown model name" multizone_supported = False udn = None if "device_info" in status: device_info = status["device_info"] capabilities = device_info.get("capabilities", {}) display_supported = capabilities.get("display_supported", True) multizone_supported = capabilities.get("multizone_supported", True) friendly_name = device_info.get("name", friendly_name) model_name = device_info.get("model_name", model_name) manufacturer = device_info.get("manufacturer", manufacturer) udn = device_info.get("ssdp_udn", None) if not display_supported: cast_type = CAST_TYPE_AUDIO if model_name.lower() == "google cast group": cast_type = CAST_TYPE_GROUP uuid = None if udn: uuid = UUID(udn.replace("-", "")) return DeviceStatus( friendly_name, model_name, manufacturer, uuid, cast_type, multizone_supported, ) except (urllib.error.HTTPError, urllib.error.URLError, OSError, ValueError): return None def _get_group_info(host, group): name = group.get("name", "Unknown group name") udn = group.get("uuid", None) uuid = None if udn: uuid = UUID(udn.replace("-", "")) elected_leader = group.get("elected_leader", "") elected_leader_split = elected_leader.rsplit(":", 1) leader_host = None leader_port = None if elected_leader == "self" and "cast_port" in group: leader_host = host leader_port = group["cast_port"] elif len(elected_leader_split) == 2: # The port in the URL is not useful, but we can scan the host leader_host = elected_leader_split[0] return MultizoneInfo(name, uuid, leader_host, leader_port) def get_multizone_status(host, services=None, zconf=None, timeout=30, context=None): """ :param host: Hostname or ip to fetch status from :type host: str :return: The multizone status as a named tuple. :rtype: pychromecast.dial.MultizoneStatus or None """ try: status = _get_status( host, services, zconf, "/setup/eureka_info?params=multizone", True, timeout, context, ) dynamic_groups = [] if "multizone" in status and "dynamic_groups" in status["multizone"]: for group in status["multizone"]["dynamic_groups"]: dynamic_groups.append(_get_group_info(host, group)) groups = [] if "multizone" in status and "groups" in status["multizone"]: for group in status["multizone"]["groups"]: groups.append(_get_group_info(host, group)) return MultizoneStatus(dynamic_groups, groups) except (urllib.error.HTTPError, urllib.error.URLError, OSError, ValueError): return None MultizoneInfo = namedtuple("MultizoneInfo", ["friendly_name", "uuid", "host", "port"]) MultizoneStatus = namedtuple("MultizoneStatus", ["dynamic_groups", "groups"]) DeviceStatus = namedtuple( "DeviceStatus", [ "friendly_name", "model_name", "manufacturer", "uuid", "cast_type", "multizone_supported", ], ) pychromecast-9.4.0/pychromecast/discovery.py000066400000000000000000000533221414324605500213200ustar00rootroot00000000000000"""Discovers Chromecasts on the network using mDNS/zeroconf.""" import abc from collections import namedtuple import functools import itertools import logging import threading import time from uuid import UUID import zeroconf from .const import CAST_TYPE_AUDIO, SERVICE_TYPE_HOST, SERVICE_TYPE_MDNS from .dial import get_device_status, get_multizone_status, get_ssl_context DISCOVER_TIMEOUT = 5 # Models matching this list will only be polled once by the HostBrowser HOST_BROWSER_BLOCKED_MODEL_PREFIXES = [ "HK", # Harman Kardon speakers crash if polled: https://github.com/home-assistant/core/issues/52020 "JBL", # JBL speakers crash if polled: https://github.com/home-assistant/core/issues/52020 ] ServiceInfo = namedtuple("ServiceInfo", ["type", "data"]) CastInfo = namedtuple( "CastInfo", ["services", "uuid", "model_name", "friendly_name", "host", "port"] ) _LOGGER = logging.getLogger(__name__) class AbstractCastListener(abc.ABC): """Listener for discovering chromecasts.""" @abc.abstractmethod def add_cast(self, uuid, service): """A cast has been discovered. uuid: The cast's uuid, this is the dictionary key to find the chromecast metadata in CastBrowser.devices. service: First known MDNS service name or host:port """ @abc.abstractmethod def remove_cast(self, uuid, service, cast_info): """A cast has been removed, meaning there are no longer any known services. uuid: The cast's uuid service: Last valid MDNS service name or host:port cast_info: CastInfo for the service to aid cleanup """ @abc.abstractmethod def update_cast(self, uuid, service): """A cast has been updated. uuid: The cast's uuid service: MDNS service name or host:port """ def _is_blocked_from_host_browser(item, block_list, item_type): for blocked_prefix in block_list: if item.startswith(blocked_prefix): _LOGGER.debug("%s %s is blocked from host based polling", item_type, item) return True return False def _is_model_blocked_from_host_browser(model): return _is_blocked_from_host_browser( model, HOST_BROWSER_BLOCKED_MODEL_PREFIXES, "Model" ) class SimpleCastListener(AbstractCastListener): """Helper for backwards compatibility.""" def __init__(self, add_callback=None, remove_callback=None, update_callback=None): self._add_callback = add_callback self._remove_callback = remove_callback self._update_callback = update_callback def add_cast(self, uuid, service): if self._add_callback: self._add_callback(uuid, service) def remove_cast(self, uuid, service, cast_info): if self._remove_callback: self._remove_callback(uuid, service, cast_info) def update_cast(self, uuid, service): if self._update_callback: self._update_callback(uuid, service) class ZeroConfListener: """Listener for ZeroConf service browser.""" def __init__(self, cast_listener, devices, host_browser, lock): self._cast_listener = cast_listener self._devices = devices self._host_browser = host_browser self._services_lock = lock def remove_service(self, _zconf, typ, name): """Called by zeroconf when an mDNS service is lost.""" _LOGGER.debug("remove_service %s, %s", typ, name) cast_info = None device_removed = False uuid = None service_info = ServiceInfo(SERVICE_TYPE_MDNS, name) # Lock because the HostBrowser may also add or remove items with self._services_lock: for uuid, info_for_uuid in self._devices.items(): if service_info in info_for_uuid.services: cast_info = info_for_uuid info_for_uuid.services.remove(service_info) if len(info_for_uuid.services) == 0: device_removed = True break if not cast_info: _LOGGER.debug("remove_service unknown %s, %s", typ, name) return if device_removed: self._cast_listener.remove_cast(uuid, name, cast_info) else: self._cast_listener.update_cast(uuid, name) def update_service(self, zconf, typ, name): """Called by zeroconf when an mDNS service is updated.""" _LOGGER.debug("update_service %s, %s", typ, name) self._add_update_service(zconf, typ, name, self._cast_listener.update_cast) def add_service(self, zconf, typ, name): """Called by zeroconf when an mDNS service is discovered.""" _LOGGER.debug("add_service %s, %s", typ, name) self._add_update_service(zconf, typ, name, self._cast_listener.add_cast) def _add_update_service(self, zconf, typ, name, callback): """Add or update a service.""" service = None tries = 0 if name.endswith("_sub._googlecast._tcp.local."): _LOGGER.debug("_add_update_service ignoring %s, %s", typ, name) return while service is None and tries < 4: try: service = zconf.get_service_info(typ, name) except IOError: # If the zeroconf fails to receive the necessary data we abort # adding the service break tries += 1 if not service: _LOGGER.debug("_add_update_service failed to add %s, %s", typ, name) return def get_value(key): """Retrieve value and decode to UTF-8.""" value = service.properties.get(key.encode("utf-8")) if value is None or isinstance(value, str): return value return value.decode("utf-8") addresses = service.parsed_addresses() host = addresses[0] if addresses else service.server # Store the host, in case mDNS stops working self._host_browser.add_hosts([host]) model_name = get_value("md") uuid = get_value("id") friendly_name = get_value("fn") if not uuid: _LOGGER.debug( "_add_update_service failed to get uuid for %s, %s", typ, name ) return # Ignore incorrect UUIDs from third-party Chromecast emulators try: uuid = UUID(uuid) except ValueError: _LOGGER.debug( "_add_update_service failed due to bad uuid for %s, %s, model %s", typ, name, model_name, ) return service_info = ServiceInfo(SERVICE_TYPE_MDNS, name) # Lock because the HostBrowser may also add or remove items with self._services_lock: if uuid not in self._devices: self._devices[uuid] = CastInfo( {service_info}, uuid, model_name, friendly_name, host, service.port ) else: # Update stored information services = self._devices[uuid].services services.add(service_info) self._devices[uuid] = CastInfo( services, uuid, model_name, friendly_name, host, service.port ) callback(uuid, name) class HostStatus: """Status of known host.""" def __init__(self): self.failcount = 0 self.no_polling = False HOSTLISTENER_CYCLE_TIME = 30 HOSTLISTENER_MAX_FAIL = 5 class HostBrowser(threading.Thread): """Repeateadly poll a set of known hosts.""" def __init__(self, cast_listener, devices, lock): super().__init__(daemon=True) self._cast_listener = cast_listener self._devices = devices self._known_hosts = {} self._next_update = time.time() self._services_lock = lock self._start_requested = False self._context = None self.stop = threading.Event() def add_hosts(self, known_hosts): """Add a list of known hosts to the set.""" for host in known_hosts: if host not in self._known_hosts: _LOGGER.debug("Addded host %s", host) self._known_hosts[host] = HostStatus() def update_hosts(self, known_hosts): """Update the set of known hosts. Note: Removed hosts will no longer be polled, but services of any associated cast devices will not be purged. """ if known_hosts is None: known_hosts = [] self.add_hosts(known_hosts) for host in list(self._known_hosts.keys()): if host not in known_hosts: _LOGGER.debug("Removed host %s", host) self._known_hosts.pop(host) def run(self): """Start worker thread.""" _LOGGER.debug("HostBrowser thread started") self._context = get_ssl_context() try: while not self.stop.is_set(): self._poll_hosts() self._next_update += HOSTLISTENER_CYCLE_TIME self.stop.wait(max(self._next_update - time.time(), 0)) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unhandled exception in worker thread") raise _LOGGER.debug("HostBrowser thread done") def _poll_hosts(self): # Iterate over a copy because other threads may modify the known_hosts list known_hosts = list(self._known_hosts.keys()) for host in known_hosts: devices = [] uuids = [] if self.stop.is_set(): break try: hoststatus = self._known_hosts[host] except KeyError: # The host has been removed by another thread continue if hoststatus.no_polling: # This host should not be polled continue device_status = get_device_status(host, timeout=30, context=self._context) if not device_status: hoststatus.failcount += 1 if hoststatus.failcount == HOSTLISTENER_MAX_FAIL: self._update_devices(host, devices, uuids) hoststatus.failcount = min( hoststatus.failcount, HOSTLISTENER_MAX_FAIL + 1 ) continue if ( device_status.cast_type != CAST_TYPE_AUDIO or _is_model_blocked_from_host_browser(device_status.model_name) ): # Polling causes frame drops on some Android TVs, # https://github.com/home-assistant/core/issues/55435 # Keep polling audio chromecasts to detect new speaker groups, but # exclude some devices which crash when polled # Note: This will not work well the IP is recycled to another cast # device. hoststatus.no_polling = True # We got device_status, try to get multizone status, then update devices hoststatus.failcount = 0 devices.append( ( 8009, device_status.friendly_name, device_status.model_name, device_status.uuid, ) ) uuids.append(device_status.uuid) multizone_status = ( get_multizone_status(host, context=self._context) if device_status.multizone_supported else None ) if multizone_status: for group in itertools.chain( multizone_status.dynamic_groups, multizone_status.groups ): # Note: This is currently (2021-02) not working for dynamic_groups, the # ports of dynamic groups are not present in the eureka_info reply. if group.host and group.host not in self._known_hosts: self.add_hosts([group.host]) if group.port is None or group.host != host: continue devices.append( ( group.port, group.friendly_name, "Google Cast Group", group.uuid, ) ) uuids.append(group.uuid) self._update_devices(host, devices, uuids) def _update_devices(self, host, devices, host_uuids): callbacks = [] # Lock because the ZeroConfListener may also add or remove items with self._services_lock: for (port, friendly_name, model_name, uuid) in devices: self._add_host_service( host, port, friendly_name, model_name, uuid, callbacks ) for uuid in self._devices: for service in self._devices[uuid].services.copy(): if ( service.type == SERVICE_TYPE_HOST and service.data[0] == host and uuid not in host_uuids ): self._remove_host_service(host, uuid, callbacks) # Handle callbacks after releasing the lock for callback in callbacks: callback() def _add_host_service(self, host, port, friendly_name, model_name, uuid, callbacks): service_info = ServiceInfo(SERVICE_TYPE_HOST, (host, port)) callback = self._cast_listener.add_cast if uuid in self._devices: callback = self._cast_listener.update_cast cast_info = self._devices[uuid] if ( service_info in cast_info.services and cast_info.model_name == model_name and cast_info.friendly_name == friendly_name ): # No changes, return return if uuid not in self._devices: self._devices[uuid] = CastInfo( {service_info}, uuid, model_name, friendly_name, host, port ) else: # Update stored information services = self._devices[uuid].services services.add(service_info) self._devices[uuid] = CastInfo( services, uuid, model_name, friendly_name, host, port ) name = f"{host}:{port}" _LOGGER.debug( "Host %s (%s) up, adding or updating host based service", name, uuid ) if callback: callbacks.append(functools.partial(callback, uuid, name)) def _remove_host_service(self, host, uuid, callbacks): if uuid not in self._devices: return info_for_uuid = self._devices[uuid] for service in info_for_uuid.services: if service.type == SERVICE_TYPE_HOST and service.data[0] == host: info_for_uuid.services.remove(service) port = service.data[1] name = f"{host}:{port}" _LOGGER.debug( "Host %s down or no longer handles uuid %s, removing host based service", name, uuid, ) if len(info_for_uuid.services) == 0: callbacks.append( functools.partial( self._cast_listener.remove_cast, uuid, name, info_for_uuid ) ) else: callbacks.append( functools.partial(self._cast_listener.update_cast, uuid, name) ) break class CastBrowser: """Discover Chromecasts on the network. When a Chromecast is found, cast_listener.add_cast is called When a Chromecast is updated, cast_listener.update_cast is called When a Chromecast is lost, the cast_listener.remove_cast is called A shared zeroconf instance can be passed as zeroconf_instance. If no instance is passed, a new instance will be created. """ def __init__(self, cast_listener, zeroconf_instance=None, known_hosts=None): self._cast_listener = cast_listener self.zc = zeroconf_instance # pylint: disable=invalid-name self._zc_browser = None self.devices = {} self.services = self.devices # For backwards compatibility self._services_lock = threading.Lock() self.host_browser = HostBrowser( self._cast_listener, self.devices, self._services_lock ) self.zeroconf_listener = ZeroConfListener( self._cast_listener, self.devices, self.host_browser, self._services_lock ) if known_hosts: self.host_browser.add_hosts(known_hosts) @property def count(self): """Number of discovered cast devices.""" return len(self.devices) def set_zeroconf_instance(self, zeroconf_instance): """Set zeroconf_instance.""" if self.zc: return self.zc = zeroconf_instance def start_discovery(self): """ This method will start discovering chromecasts on separate threads. When a chromecast is discovered, callback will be called with the discovered chromecast's zeroconf name. This is the dictionary key to find the chromecast metadata in CastBrowser.devices. A shared zeroconf instance can be passed as zeroconf_instance. If no instance is passed, a new instance will be created. """ if self.zc: self._zc_browser = zeroconf.ServiceBrowser( self.zc, "_googlecast._tcp.local.", self.zeroconf_listener, ) self.host_browser.start() def stop_discovery(self): """Stop the chromecast discovery threads.""" if self._zc_browser: try: self._zc_browser.cancel() except RuntimeError: # Throws if called from service callback when joining the zc browser thread pass self._zc_browser.zc.close() self.host_browser.stop.set() self.host_browser.join() class CastListener(CastBrowser): """Backwards compatible helper class.""" def __init__(self, add_callback=None, remove_callback=None, update_callback=None): _LOGGER.info("CastListener is deprecated, update to use CastBrowser instead") listener = SimpleCastListener(add_callback, remove_callback, update_callback) super().__init__(listener) def start_discovery(cast_browser, zeroconf_instance): """Start discovering chromecasts on the network.""" _LOGGER.info( "start_discovery is deprecated, call cast_browser.start_discovery() instead" ) cast_browser.set_zeroconf_instance(zeroconf_instance) cast_browser.start_discovery() return cast_browser def stop_discovery(cast_browser): """Stop the chromecast discovery threads.""" _LOGGER.info( "stop_discovery is deprecated, call cast_browser.stop_discovery() instead" ) cast_browser.stop_discovery() def discover_chromecasts( max_devices=None, timeout=DISCOVER_TIMEOUT, zeroconf_instance=None, known_hosts=None ): """ Discover chromecasts on the network. Returns a tuple of: A list of chromecast devices, or an empty list if no matching chromecasts were found. A service browser to keep the Chromecast mDNS data updated. When updates are (no longer) needed, call browser.stop_discovery(). :param zeroconf_instance: An existing zeroconf instance. """ def add_callback(_uuid, _service): """Called when zeroconf has discovered a new chromecast.""" if max_devices is not None and browser.count >= max_devices: discover_complete.set() discover_complete = threading.Event() zconf = zeroconf_instance or zeroconf.Zeroconf() browser = CastBrowser(SimpleCastListener(add_callback), zconf, known_hosts) browser.start_discovery() # Wait for the timeout or the maximum number of devices discover_complete.wait(timeout) return (list(browser.devices.values()), browser) def discover_listed_chromecasts( friendly_names=None, uuids=None, discovery_timeout=DISCOVER_TIMEOUT, zeroconf_instance=None, known_hosts=None, ): """ Searches the network for chromecast devices matching a list of friendly names or a list of UUIDs. Returns a tuple of: A list of chromecast devices matching the criteria, or an empty list if no matching chromecasts were found. A service browser to keep the Chromecast mDNS data updated. When updates are (no longer) needed, call browser.stop_discovery(). :param friendly_names: A list of wanted friendly names :param uuids: A list of wanted uuids :param discovery_timeout: A floating point number specifying the time to wait devices matching the criteria have been found. :param zeroconf_instance: An existing zeroconf instance. """ cc_list = {} def add_callback(uuid, service): _LOGGER.debug("Got cast %s, %s", uuid, service) service = browser.devices[uuid] friendly_name = service[3] if uuids and uuid in uuids: cc_list[uuid] = browser.devices[uuid] uuids.remove(uuid) if friendly_names and friendly_name in friendly_names: cc_list[uuid] = browser.devices[uuid] friendly_names.remove(friendly_name) if not friendly_names and not uuids: discover_complete.set() discover_complete = threading.Event() zconf = zeroconf_instance or zeroconf.Zeroconf() browser = CastBrowser(SimpleCastListener(add_callback), zconf, known_hosts) browser.start_discovery() # Wait for the timeout or found all wanted devices discover_complete.wait(discovery_timeout) return (list(cc_list.values()), browser) pychromecast-9.4.0/pychromecast/error.py000066400000000000000000000023361414324605500204410ustar00rootroot00000000000000""" Errors to be used by PyChromecast. """ class PyChromecastError(Exception): """Base error for PyChromecast.""" class NoChromecastFoundError(PyChromecastError): """ When a command has to auto-discover a Chromecast and cannot find one. """ class MultipleChromecastsFoundError(PyChromecastError): """ When getting a singular chromecast results in getting multiple chromecasts. """ class ChromecastConnectionError(PyChromecastError): """When a connection error occurs within PyChromecast.""" class LaunchError(PyChromecastError): """When an app fails to launch.""" class PyChromecastStopped(PyChromecastError): """Raised when a command is invoked while the Chromecast's socket_client is stopped. """ class NotConnected(PyChromecastError): """ Raised when a command is invoked while not connected to a Chromecast. """ class UnsupportedNamespace(PyChromecastError): """ Raised when trying to send a message with a namespace that is not supported by the current running app. """ class ControllerNotRegistered(PyChromecastError): """ Raised when trying to interact with a controller while it is not registered with a ChromeCast object. """ pychromecast-9.4.0/pychromecast/logging_pb2.py000066400000000000000000001072371414324605500215070ustar00rootroot00000000000000# 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-9.4.0/pychromecast/quick_play.py000066400000000000000000000051571414324605500214550ustar00rootroot00000000000000""" Choose a controller and quick play """ from .controllers.youtube import YouTubeController from .controllers.supla import SuplaController from .controllers.yleareena import YleAreenaController from .controllers.bubbleupnp import BubbleUPNPController from .controllers.bbciplayer import BbcIplayerController from .controllers.bbcsounds import BbcSoundsController from .controllers.homeassistant_media import HomeAssistantMediaController def quick_play(cast, app_name, data): """ Given a Chromecast connection, launch the app `app_name` and start playing media based on parameters defined in `data`. :param cast: Chromecast connection to cast to :param app_name: App name "slug" to cast :param data: Data to send to the app controller. Must contain "media_id", and other values can be passed depending on the controller. :type cast: Chromecast :type app_name: string :type data: dict `data` can contain the following keys: media_id: string (Required) Primary identifier of the media media_type: string Type of the media identified by `media_id`. e.g. "program" if the media is a program name instead of a direct item id. When using a regular media controller (e.g. BubbleUPNP) this should be the content_type ('audio/mp3') enqueue: boolean Enqueue the media to the current playlist, if possible. index: string Play index x of matching media. "random" should also be allowed. audio_lang: string Audio language (3 characters for YleAreena) text_lang: string Subtitle language (3 characters for YleAreena) Youtube-specific: playlist_id: string Youtube playlist id Supla-specific: is_live: boolean Whether the media is a livestream Media controller (BubbleUPNP)-specific: stream_type: string "BUFFERED" or "LIVE" """ if app_name == "youtube": controller = YouTubeController() elif app_name == "supla": controller = SuplaController() elif app_name == "yleareena": controller = YleAreenaController() elif app_name == "bubbleupnp": controller = BubbleUPNPController() elif app_name == "bbciplayer": controller = BbcIplayerController() elif app_name == "bbcsounds": controller = BbcSoundsController() elif app_name == "homeassistant_media": controller = HomeAssistantMediaController() else: raise NotImplementedError() cast.register_handler(controller) controller.quick_play(**data) pychromecast-9.4.0/pychromecast/socket_client.py000066400000000000000000001143541414324605500221420ustar00rootroot00000000000000""" 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: disable=too-many-lines import abc import errno import json import logging import select import socket import ssl import sys import threading import time from collections import namedtuple from struct import pack, unpack from . import cast_channel_pb2 from .controllers import BaseController from .controllers.media import MediaController from .controllers.receiver import ReceiverController from .const import CAST_TYPE_CHROMECAST, MESSAGE_TYPE, REQUEST_ID, SESSION_ID from .dial import get_host_from_service from .error import ( ChromecastConnectionError, UnsupportedNamespace, NotConnected, PyChromecastStopped, ) NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection" NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat" PLATFORM_DESTINATION_ID = "receiver-0" TYPE_PING = "PING" TYPE_PONG = "PONG" TYPE_CONNECT = "CONNECT" TYPE_CLOSE = "CLOSE" 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" # Failed to resolve service name CONNECTION_STATUS_FAILED_RESOLVE = "FAILED_RESOLVE" # The socket connection was lost and needs to be retried CONNECTION_STATUS_LOST = "LOST" 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.""" def _dict_from_message_payload(message): """Parses a PB2 message as a JSON dict.""" try: data = json.loads(message.payload_utf8) if not isinstance(data, dict): logger = logging.getLogger(__name__) logger.debug( "Non dict json in namespace %s: '%s'", message.namespace, message.payload_utf8, ) return {} return data except ValueError: logger = logging.getLogger(__name__) logger.debug( "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 = _dict_from_message_payload(message) return ( f"Message {message.namespace} from {message.source_id} to " f"{message.destination_id}: {data or message.payload_utf8}" ) 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"]) class ConnectionStatusListener(abc.ABC): """Listener for receiving connection status events.""" @abc.abstractmethod def new_connection_status(self, status: ConnectionStatus): """Updated connection status.""" # pylint: disable=too-many-instance-attributes class SocketClient(threading.Thread): """ Class to interact with a Chromecast through a socket. :param host: The host to connect to. :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 infinite 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. :param services: A list of mDNS services to try to connect to. If present, parameters host and port are ignored and host and port are instead resolved through mDNS. The list of services may be modified, for example if speaker group leadership is handed over. SocketClient will catch modifications to the list when attempting reconnect. :param zconf: A zeroconf instance, needed if a list of services is passed. The zeroconf instance may be obtained from the browser returned by pychromecast.start_discovery(). """ 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) services = kwargs.pop("services", None) zconf = kwargs.pop("zconf", None) super().__init__() self.daemon = True self.logger = logging.getLogger(__name__) self._force_recon = False self.cast_type = cast_type self.fn = None # pylint:disable=invalid-name self.tries = tries self.timeout = timeout or TIMEOUT_TIME self.retry_wait = retry_wait or RETRY_TIME self.host = host self.services = services or [None] self.zconf = zconf self.port = port or 8009 self.source_id = "sender-0" self.stop = threading.Event() # socketpair used to interrupt the worker thread self.socketpair = socket.socketpair() 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.first_connection = True self.socket = None # dict mapping namespace on Controller objects self._handlers = {} self._connection_listeners = [] self.receiver_controller = ReceiverController(cast_type) 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) def initialize_connection( self, ): # pylint:disable=too-many-statements, too-many-branches """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 # Dict keeping track of individual retry delay for each named service retries = {} def mdns_backoff(service, retry): """Exponentional backoff for service name mdns lookups.""" now = time.time() retry["next_retry"] = now + retry["delay"] retry["delay"] = min(retry["delay"] * 2, 300) retries[service] = retry while not self.stop.is_set() and ( tries is None or tries > 0 ): # pylint:disable=too-many-nested-blocks # Prune retries dict retries = { key: retries[key] for key in self.services.copy() if (key is not None and key in retries) } for service in self.services.copy(): now = time.time() retry = retries.get( service, {"delay": self.retry_wait, "next_retry": now} ) # If we're connecting to a named service, check if it's time if service and now < retry["next_retry"]: continue try: self.socket = new_socket() self.socket.settimeout(self.timeout) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_CONNECTING, NetworkAddress(self.host, self.port), ) ) # Resolve the service name. If service is None, we're # connecting directly to a host name or IP-address if service: host = None port = None host, port, service_info = get_host_from_service( service, self.zconf ) if host and port: if service_info: try: self.fn = service_info.properties[b"fn"].decode( "utf-8" ) except (AttributeError, KeyError, UnicodeError): pass self.logger.debug( "[%s(%s):%s] Resolved service %s to %s:%s", self.fn or "", self.host, self.port, service, host, port, ) self.host = host self.port = port else: self.logger.debug( "[%s(%s):%s] Failed to resolve service %s", self.fn or "", self.host, self.port, service, ) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_FAILED_RESOLVE, NetworkAddress(service, None), ) ) mdns_backoff(service, retry) # If zeroconf fails to receive the necessary data, # try next service continue self.logger.debug( "[%s(%s):%s] Connecting to %s:%s", self.fn or "", self.host, self.port, self.host, self.port, ) self.socket.connect((self.host, self.port)) context = ssl.SSLContext() self.socket = context.wrap_socket(self.socket) 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() if self.first_connection: self.first_connection = False self.logger.debug( "[%s(%s):%s] Connected!", self.fn or "", self.host, self.port, ) else: self.logger.info( "[%s(%s):%s] Connection reestablished!", self.fn or "", self.host, self.port, ) return except OSError as err: self.connecting = True if self.stop.is_set(): self.logger.error( "[%s(%s):%s] Failed to connect: %s. aborting due to stop signal.", self.fn or "", self.host, self.port, err, ) raise ChromecastConnectionError("Failed to connect") from err self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_FAILED, NetworkAddress(self.host, self.port), ) ) if service is not None: retry_log_fun( "[%s(%s):%s] Failed to connect to service %s, retrying in %.1fs", self.fn or "", self.host, self.port, service, retry["delay"], ) mdns_backoff(service, retry) else: retry_log_fun( "[%s(%s):%s] Failed to connect, retrying in %.1fs", self.fn or "", self.host, self.port, self.retry_wait, ) retry_log_fun = self.logger.debug # Only sleep if we have another retry remaining if tries is None or tries > 1: self.logger.debug( "[%s(%s):%s] Not connected, sleeping for %.1fs. Services: %s", self.fn or "", self.host, self.port, self.retry_wait, self.services, ) time.sleep(self.retry_wait) if tries: tries -= 1 self.stop.set() self.logger.error( "[%s(%s):%s] Failed to connect. No retries.", self.fn or "", self.host, self.port, ) raise ChromecastConnectionError("Failed to connect") def connect(self): """Connect socket connection to Chromecast device. Must only be called if the worker thread will not be started. """ try: self.initialize_connection() except ChromecastConnectionError: self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port) ) ) return def disconnect(self): """Disconnect socket connection to Chromecast device""" self.stop.set() try: # Write to the socket to interrupt the worker thread self.socketpair[1].send(b"x") except socket.error: # The socketpair may already be closed during shutdown, ignore it pass def register_handler(self, handler: BaseController): """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): """Connect to the cast and start polling the socket.""" try: self.initialize_connection() except ChromecastConnectionError: self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port) ) ) return self.heartbeat_controller.reset() self._force_recon = False self.logger.debug("Thread started...") try: while not self.stop.is_set(): if self.run_once(timeout=POLL_TIME_BLOCKING) == 1: break except Exception: # pylint: disable=broad-except self.logger.exception( ("[%s(%s):%s] Unhandled exception in worker thread"), self.fn or "", self.host, self.port, ) raise self.logger.debug("Thread done...") # Clean up self._cleanup() def run_once(self, timeout=POLL_TIME_NON_BLOCKING): """ Use run_once() in your own main loop after you receive something on the socket (get_socket()). """ # pylint: disable=too-many-branches, too-many-return-statements try: if not self._check_connection(): return 0 except ChromecastConnectionError: return 1 # poll the socket, as well as the socketpair to allow us to be interrupted rlist = [self.socket, self.socketpair[0]] try: can_read, _, _ = select.select(rlist, [], [], timeout) except (ValueError, OSError) as exc: self.logger.error( "[%s(%s):%s] Error in select call: %s", self.fn or "", self.host, self.port, exc, ) self._force_recon = True return 0 # 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( "[%s(%s):%s] Stopped while reading message, disconnecting.", self.fn or "", self.host, self.port, ) else: self.logger.error( "[%s(%s):%s] Interruption caught without being stopped: %s", self.fn or "", self.host, self.port, 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( "[%s(%s):%s] Error reading from socket.", self.fn or "", self.host, self.port, ) else: data = _dict_from_message_payload(message) if self.socketpair[0] in can_read: # Clear the socket's buffer self.socketpair[0].recv(128) # If we are stopped after receiving a message we skip the message # and tear down the connection if self.stop.is_set(): return 1 if not message: return 0 # 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) return 0 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( "[%s(%s):%s] Error communicating with socket, resetting connection", self.fn or "", self.host, self.port, ) reset = True elif self.heartbeat_controller.is_expired(): self.logger.warning( "[%s(%s):%s] Heartbeat timeout, resetting connection", self.fn or "", self.host, self.port, ) reset = True if reset: self.receiver_controller.disconnected() for channel in self._open_channels: self.disconnect_channel(channel) 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: dict): """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( "[%s(%s):%s] Received: %s", self.fn or "", self.host, self.port, _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( "[%s(%s):%s] Message unhandled: %s", self.fn or "", self.host, self.port, _message_to_string(message, data), ) except Exception: # pylint: disable=broad-except self.logger.exception( ( "[%s(%s):%s] Exception caught while sending message to " "controller %s: %s" ), self.fn or "", self.host, self.port, type(self._handlers[message.namespace]).__name__, _message_to_string(message, data), ) else: self.logger.debug( "[%s(%s):%s] Received unknown namespace: %s", self.fn or "", self.host, self.port, _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 if self.socket is not None: try: self.socket.close() except Exception: # pylint: disable=broad-except self.logger.exception( "[%s(%s):%s] _cleanup", self.fn or "", self.host, self.port ) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port) ) ) self.socketpair[0].close() self.socketpair[1].close() 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( "[%s(%s):%s] connection listener: %x (%s) %s", self.fn or "", self.host, self.port, id(listener), type(listener).__name__, status, ) listener.new_connection_status(status) except Exception: # pylint: disable=broad-except self.logger.exception( "[%s(%s):%s] Exception thrown when calling connection listener", self.fn or "", self.host, self.port, ) 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: self.logger.debug( "[%s(%s):%s] timeout in : _read_bytes_from_socket", self.fn or "", self.host, self.port, ) continue except ssl.SSLError as exc: # Support older ssl implementations which does not raise # socket.timeout on timeouts if _is_ssl_timeout(exc): self.logger.debug( "[%s(%s):%s] ssl timeout in : _read_bytes_from_socket", self.fn or "", self.host, self.port, ) 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) 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 msg = cast_channel_pb2.CastMessage() msg.protocol_version = msg.CASTV2_1_0 # pylint: disable=no-member msg.source_id = self.source_id msg.destination_id = destination_id msg.payload_type = ( cast_channel_pb2.CastMessage.STRING # pylint: disable=no-member ) 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: # pylint: disable=no-member self.logger.debug( "[%s(%s):%s] Sending: %s", self.fn or "", self.host, self.port, _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: if not no_add_request_id and callback_function: self._request_callbacks[request_id] = { "event": threading.Event(), "response": None, "function": callback_function, } self.socket.sendall(be_size + msg.SerializeToString()) except socket.error: self._request_callbacks.pop(request_id, None) self._force_recon = True self.logger.info( "[%s(%s):%s] Error writing to socket.", self.fn or "", self.host, self.port, ) else: raise NotConnected("Chromecast {self.host}:{self.port} is connecting...") 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( f"Namespace {namespace} is not supported by current app. " f"Supported are {', '.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: ConnectionStatusListener): """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: try: self.send_message( destination_id, NS_CONNECTION, {MESSAGE_TYPE: TYPE_CLOSE, "origin": {}}, no_add_request_id=True, force=True, ) except NotConnected: pass except Exception: # pylint: disable=broad-except self.logger.exception( "[%s(%s):%s] Exception", self.fn or "", self.host, self.port ) 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().__init__(NS_CONNECTION) def receive_message(self, message, data: dict): """ Called when a message is received. data is message.payload_utf8 interpreted as a JSON dict. """ if self._socket_client.is_stopped: return True if data[MESSAGE_TYPE] == TYPE_CLOSE: # The cast device is asking us to acknowledge closing this channel. self._socket_client.disconnect_channel(message.source_id) # Schedule a status update so that a channel is created. self._socket_client.receiver_controller.update_status() return True return False class HeartbeatController(BaseController): """Controller to respond to heartbeat messages.""" def __init__(self): super().__init__(NS_HEARTBEAT, target_platform=True) self.last_ping = 0 self.last_pong = time.time() def receive_message(self, _message, data: dict): """ Called when a heartbeat message is received. data is message.payload_utf8 interpreted as a JSON dict. """ 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 if data[MESSAGE_TYPE] == TYPE_PONG: self.reset() return True 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 def new_socket(): """ Create a new socket with OS-specific parameters Try to set SO_REUSEPORT for BSD-flavored systems if it's an option. Catches errors if not. """ new_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) new_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: # noinspection PyUnresolvedReferences reuseport = socket.SO_REUSEPORT except AttributeError: pass else: try: new_sock.setsockopt(socket.SOL_SOCKET, reuseport, 1) except (OSError, socket.error) as err: # OSError on python 3, socket.error on python 2 if err.errno != errno.ENOPROTOOPT: raise return new_sock pychromecast-9.4.0/pylintrc000066400000000000000000000005241414324605500160210ustar00rootroot00000000000000[MASTER] ignore=cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py reports=no disable= format, locally-disabled, too-few-public-methods, too-many-arguments, too-many-instance-attributes, too-many-public-methods, duplicate-code, too-many-nested-blocks, [EXCEPTIONS] overgeneral-exceptions=Exception,PyChromecastError pychromecast-9.4.0/pyproject.toml000066400000000000000000000000351414324605500171430ustar00rootroot00000000000000[tool.black] exclude = 'pb2' pychromecast-9.4.0/requirements-test.txt000066400000000000000000000000541414324605500204710ustar00rootroot00000000000000flake8==4.0.1 pylint==2.11.1 black==21.10b0 pychromecast-9.4.0/requirements.txt000066400000000000000000000000611414324605500175120ustar00rootroot00000000000000protobuf>=3.0.0 zeroconf>=0.25.1 casttube>=0.2.0 pychromecast-9.4.0/script/000077500000000000000000000000001414324605500155355ustar00rootroot00000000000000pychromecast-9.4.0/script/release000077500000000000000000000002111414324605500170750ustar00rootroot00000000000000#!/bin/sh # Pushes a new version to PyPi. rm -rf dist python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing pychromecast-9.4.0/setup.cfg000066400000000000000000000004321414324605500160510ustar00rootroot00000000000000[wheel] universal = 1 [flake8] # To work with Black max-line-length = 88 # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring ignore = E501, W503, E203, D202 pychromecast-9.4.0/setup.py000066400000000000000000000016031414324605500157430ustar00rootroot00000000000000from setuptools import setup, find_packages long_description = open("README.rst").read() setup( name="PyChromecast", version="9.4.0", 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 :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ], )