Skip to content

Commit

Permalink
feat: Cast a website to Chromecast (via DashCast) (#102)
Browse files Browse the repository at this point in the history
* Add Support for DashCast

* One commit solely honoring flake8

* it works but the code looks awful

* it's looking better

* the code looks good now

* oops

* minor changes to setup_cast

* minor comment, for clarity

* we now make good use of CastStatusListener

* _prep_control will again consider a webpage an inactive chromecast

* better way to use CastStatusListener

* updated readme and comments

* final changes?

* small changes

* DashCast does not need to fiddle with backdrop anymore

* Minor text fixes

* remove unused lines

* Big-ass comment about why this is here here.
  • Loading branch information
marcosdiez authored and skorokithakis committed May 23, 2018
1 parent f0fd4c3 commit b66f205
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 12 deletions.
26 changes: 21 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Cast All The Things

Cast All The Things allows you to send videos from many, many online sources
(YouTube, Vimeo, and a few hundred others) to your Chromecast. It also allows
you to cast local files.
you to cast local files or render websites.


Installation
Expand All @@ -35,14 +35,27 @@ To use Cast All The Things, just specify a URL::

catt cast "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

CATT supports any service that youtube-dl supports, which includes most online
``catt`` supports any service that youtube-dl supports, which includes most online
video hosting services.

CATT can also cast local files (if they're in a format the Chromecast supports
``catt`` can also cast local files (if they're in a format the Chromecast supports
natively)::

catt cast ./myvideo.mp4

If you have subtitles and the name is similar to the name of the local file, ``catt`` will add them automatically.
You can, of course, specify any other subtitle if you want. Although Chromecast only supports WEBVTT,
TTML and Line 21 subtitles, ``catt`` conveniently converts SRTs to WEBVTT for you on the fly. Here is how to use it:

catt cast -s ./mysubtitle.srt /myvideo.mp4

``catt`` can also tell your Chromecast to display any website::

catt cast_url https://en.wikipedia.org/wiki/Rickrolling

Please note that the Chromecast has a slow CPU but a reasonably recent version of Google Chrome. The display
resolution is 1280x720.

You can also control your Chromecast through ``catt`` commands, for example with
``catt pause``. Try running ``catt --help`` to see the full list of commands.

Expand Down Expand Up @@ -78,11 +91,11 @@ edit ``catt.cfg``
Contributing
------------

If you want to contribute a feature to CATT, please open an issue (or comment on
If you want to contribute a feature to ``catt``, please open an issue (or comment on
an existing one) first, to make sure it's something that the maintainers are
interested in. Afterwards, just clone the repository and hack away!

To run CATT in development, you can use the following command::
To run ``catt`` in development, you can use the following command::

python -m catt.cli --help

Expand All @@ -99,3 +112,6 @@ Features

* Casts videos to Chromecast.
* From `many, many online sources <http://rg3.github.io/youtube-dl/supportedsites.html>`_.
* Casts local files (videos, photos and music)
* Casts any website to Chromecast

9 changes: 9 additions & 0 deletions catt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ def process_subtitle(ctx, param, value):
return value


@cli.command(short_help="Cast any webpage to Chromecast")
@click.argument("url")
@click.pass_obj
def cast_url(settings, url):
cst = setup_cast(settings["device"], prep="app", controller="dashcast")
click.echo("Casting URL %s on \"%s\"..." % (url, cst.cc_name))
cst.load_url(url)


@cli.command(short_help="Send a video to a Chromecast for playing.")
@click.argument("video_url", callback=process_url)
@click.option("-s", "--subtitle",
Expand Down
54 changes: 47 additions & 7 deletions catt/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@

import pychromecast
from click import ClickException, echo
from pychromecast.controllers.dashcast import APP_DASHCAST as DASHCAST_APP_ID
from pychromecast.controllers.dashcast import DashCastController as PyChromecastDashCastController

from .stream_info import StreamInfo
from .util import warning
from .youtube import YouTubeController


APP_INFO = [{"app_name": "youtube", "app_id": "233637DE", "supported_device_types": ["cast"]}]
APP_INFO = [
{"app_name": "youtube", "app_id": "233637DE", "supported_device_types": ["cast"]},
{"app_name": "dashcast", "app_id": DASHCAST_APP_ID, "supported_device_types": ["cast"]},
]
DEFAULT_APP = {"app_name": "default", "app_id": "CC1AD845"}
BACKDROP_APP_ID = "E8C28D3C"

Expand Down Expand Up @@ -110,6 +115,10 @@ def setup_cast(device_name, video_url=None, prep=None, controller=None):

if app["app_name"] == "youtube":
controller = YoutubeCastController(cast, app["app_name"], app["app_id"], prep=prep)
elif app["app_name"] == "dashcast":
if cast.cast_type not in app["supported_device_types"]:
raise CattCastError("The %s app is not available for this device." % app["app_name"].capitalize())
controller = DashCastController(cast, app["app_name"], app["app_id"], prep=prep)
else:
controller = DefaultCastController(cast, app["app_name"], app["app_id"], prep=prep)
return (controller, stream) if stream else controller
Expand Down Expand Up @@ -223,15 +232,32 @@ class CastStatusListener:
def __init__(self, app_id, active_app_id):
self.app_id = app_id
self.app_ready = threading.Event()
if app_id == active_app_id:
if app_id == active_app_id and app_id != DASHCAST_APP_ID:
self.app_ready.set()

def new_cast_status(self, status):
if status.app_id == self.app_id:
if self._is_app_ready(status):
self.app_ready.set()
else:
self.app_ready.clear()

def _is_app_ready(self, status):
if status.app_id == self.app_id == DASHCAST_APP_ID:
# DashCast is an exception and therefore needs special treatment.
# Whenever it's loaded, it's initial status is "Application is starting",
# as shown here: https://github.com/stestagg/dashcast/blob/master/receiver.html#L163
# While in that status, it's still not ready to start receiving nor loading URLs
# Therefore we must wait until its status change to "Application ready"
# https://github.com/stestagg/dashcast/blob/master/receiver.html#L143
#
# If one does not wait for the status to become "Application ready",
# casting the URL will trigger a race condition as the URL may arrive before the
# "Application ready" status. In this case, casting will not work.
# One simple way to confirm changes is to uncomment the line below
# print(status.status_text)
return status.status_text == "Application ready"
return status.app_id == self.app_id


class MediaStatusListener:
def __init__(self, state):
Expand Down Expand Up @@ -325,15 +351,13 @@ def _is_seekable(self):
status.stream_type == "BUFFERED") else False

def _prep_app(self):
"""Make shure desired chromecast app is running."""

"""Make sure desired chromecast app is running."""
if not self._cast_listener.app_ready.is_set():
self._cast.start_app(self._cast_listener.app_id)
self._cast_listener.app_ready.wait()

def _prep_control(self):
"""Make shure chromecast is in an active state."""

"""Make sure chromecast is in an active state."""
if self._cast.app_id == BACKDROP_APP_ID or not self._cast.app_id:
raise CattCastError("Chromecast is inactive.")
self._cast.media_controller.block_until_active(1.0)
Expand Down Expand Up @@ -447,6 +471,22 @@ def restore(self, data):
title=data["title"], thumb=data["thumb"])


class DashCastController(CastController):
def __init__(self, cast, name, app_id, prep=None):
self._controller = PyChromecastDashCastController()
super(DashCastController, self).__init__(cast, name, app_id, prep=prep)

def load_url(self, url, **kwargs):
self._controller.load_url(url, force=True)

def _prep_app(self):
"""Make sure desired chromecast app is running."""
# we must force the launch of the DashCast app because it, by design,
# becomes unresponsive after a website is loaded
self._cast.socket_client.receiver_controller.launch_app(self._cast_listener.app_id, force_launch=True)
self._cast_listener.app_ready.wait()


class YoutubeCastController(CastController):
def __init__(self, cast, name, app_id, prep=None):
self._controller = YouTubeController()
Expand Down

0 comments on commit b66f205

Please sign in to comment.