-
Notifications
You must be signed in to change notification settings - Fork 155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support to Cast Any Website to Chromecast (via DashCast) #102
Conversation
Thank you for the PR! I'm a bit apprehensive about the mode of failure where the URL mistakenly doesn't return a video and the site itself gets cast. Maybe this should be an explicit switch that would force casting a site? @theychx, what do you think? |
I though a lot about that. Maybe have a completely different command, like I can easily implement it after a conclusion is achieved! |
Yes, I think a dedicated command might be much better. I think this also fits with the spirit of catt, generally, and the code doesn't seem very complicated. Let's wait for @theychx to chime in as well. |
my code is broken. please don't merge. I am adding the |
I'm not keen on the current approach. Making a separate command seems like the obvious solution, as this would mean that you could just call |
Yes, I'm keen on that for the same reason, it would remove much of the |
@mdiez-modus |
|
Yeah, or just |
I think |
|
The official terminology that chrome uses is "tab mirroring", so this kind of keeps in line with that. |
Sorry @skorokithakis but that's completely different. What Chrome does is open the tab in itself and mirror what it has rendered to Chromecast. |
Hmm, yes, good point. Do you want to make the changes first, and decide on the name later? Renaming the command is a one-line change anyway, and I think we have consensus that this is a useful feature to include. |
The idea of making this a separate URL really made everything easier. I did not like to have to change I also made sure we can reload URLs, something that was previously not possible. We now have only to decide how the command will be called. I let to you guys decide. I am happy with the functionality! Thanks for the code review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is working nicely, although it's a real shame we can't utilize the reload functionality of the controller.
catt/controllers.py
Outdated
@@ -334,11 +343,13 @@ def _prep_app(self): | |||
def _prep_control(self): | |||
"""Make shure chromecast is in an active state.""" | |||
|
|||
if self._cast.app_id == DASHCAST_APP_ID: | |||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hack, please remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a hack. With this line, it's possible to type catt info
and see the title of the current loaded webpage. Without it, we get a boring Error: Nothing is currently playing.
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also allows the user to use catt ffwd 60
while a webpage is loaded, and then get the irrelevant error message Error: Stream is not seekable.
info
and status
displays video/audio info (as descibed in the command helptexts).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok... you convinced me!
catt/controllers.py
Outdated
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) | ||
if self._cast.media_controller.status.player_state in ["UNKNOWN", "IDLE"]: | ||
raise CattCastError("Nothing is currently playing.") | ||
raise CattCastError("Nothing is currently playing. [APP_ID={}]".format(self._cast.app_id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The app id is not useful to the regular user (not in this context at least).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was useful to me, but I don't mind removing it.
catt/controllers.py
Outdated
self._load_url(url) # this will actually take us to the "Waiting for URL" screen | ||
while not self._is_waiting_for_url(): | ||
time.sleep(1) | ||
self._load_url(url) # this will finally load the URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have found a way to make this work without going through these hoops:
Override _prep_app
in your dashcast
controller, so that it always kills the running app.
Add a backdrop_ready
flag to CastStatusListener
.
In the overridden prep_app
, kill the running app and wait for the flag to be set.
catt/cli.py
Outdated
@@ -181,6 +181,15 @@ def process_subtitle(ctx, param, value): | |||
return value | |||
|
|||
|
|||
@cli.command(short_help="Makes your Chromecast display any webpage") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the imperative mood, like in the other command help texts.
@@ -110,8 +116,11 @@ def setup_cast(device_name, video_url=None, prep=None, controller=None): | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to fail here, if the type of the chosen cast device is not in supported_device_types
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of failing, I decided to just print a warning for two reasons:
- this is what
catt
currently do for other stuff - warnings are more future proof than failures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A warning is not going to cut it. The warning logic in setup_cast
is currently used to make sure that the default cast controller is returned to cli.py
when the selected device is not supported by a custom controller. In cli.py
there is then a fallback when trying to cast a playlist with the default castcontroller. With your functionality, there is no fallback, so we need to fail.
(and you print a warning, and then return the dashcast castcontroller anyway.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more notes.
@@ -110,8 +116,11 @@ def setup_cast(device_name, video_url=None, prep=None, controller=None): | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A warning is not going to cut it. The warning logic in setup_cast
is currently used to make sure that the default cast controller is returned to cli.py
when the selected device is not supported by a custom controller. In cli.py
there is then a fallback when trying to cast a playlist with the default castcontroller. With your functionality, there is no fallback, so we need to fail.
(and you print a warning, and then return the dashcast castcontroller anyway.)
catt/controllers.py
Outdated
self.app_ready = threading.Event() | ||
self.set_app_id(app_id, active_app_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please just add a backdrop_ready
flag of the threading.Event
kind, and then initiate it like app_ready
, and expand the logic in new_cast_status
so the flag is set/cleared in the same manner as app_ready
.
catt/controllers.py
Outdated
self._cast_listener.set_app_id(DASHCAST_APP_ID) | ||
|
||
self._cast.start_app(DASHCAST_APP_ID) | ||
self._cast_listener.app_ready.wait() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine _prep_app
being as simple as this:
self.kill()
self._cast_listener.backdrop_ready.wait()
self._cast.start_app(self._cast_listener.app_id)
self._cast_listener.app_ready.wait()
@theychx I did all the changes you requested. I believe it should be ready to be merged now! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more note.
catt/controllers.py
Outdated
self.app_ready = threading.Event() | ||
self.set_app_id(app_id, active_app_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I wanted was this:
class CastStatusListener:
def __init__(self, app_id, active_app_id):
self.app_id = app_id
self.app_ready = threading.Event()
self.backdrop_ready = threading.Event()
if active_app_id == app_id:
self.app_ready.set()
elif active_app_id == BACKDROP_APP_ID:
self.backdrop_ready.set()
def new_cast_status(self, status):
if status.app_id == self.app_id:
self.app_ready.set()
self.backdrop_ready.clear()
elif status.app_id == BACKDROP_APP_ID:
self.backdrop_ready.set()
self.app_ready.clear()
else:
self.app_ready.clear()
self.backdrop_ready.clear()
You might want to know that after the listener has been registered in the constructor of the CastController
baseclass, pychromecast
automatically calls the new_cast_status
method of the listener object whenever there is a change.
Also, threading.Event
flags are initially set to false.
I see. It all makes sense. By the way I was reading pychromecast's source code and I think there is a way to bypass backdrop completelly. I will test tomorrow after work |
@theychx now it's G R E A T ! I will soon write a patch to pychromecast. |
That's pretty fantastic. Hopefully your patch will stabilize the API so it doesn't change from under our feet. |
My PR to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great find!
catt/controllers.py
Outdated
# 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.start_app(self._cast_listener.app_id) | ||
self._cast_listener.app_ready.wait() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you clear the app_ready
flag before force launching the app, it would not be necessary to modify CastStatusListener
or use the class variable. Also the self._cast.start_app
call is not needed anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't not modifying CastStatusListener
make things rely on a race condition ?
The modified version of CastStatusListener
guarantees that the app is only ready when it should be.
The DashCast app has an "Application Is Starting" state in which it can not yet receive URLs: https://github.com/stestagg/dashcast/blob/master/receiver.html#L163
This is one of the reasons, actually, that I liked that catt info
showed the state of the DashCast app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't not modifying CastStatusListener make things rely on a race condition ?
Hmm, I guess thats true, although it's extremely unlikely that this would ever be a real problem. I've tested my own suggestion and could not get it to fail.
I'm keen on the code needed to circumvent the odd behaviour of dashcast, being contained within the overridden _prep_app
. And I am honestly not keen on the class variable and the use of it in CastStatusListener
.
@skorokithakis I'm not completely sure I'm in the right here, any input?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not like the change either on CastStatusListener
but either we leave things as is or we patch DashCast
to behave as expected. Race conditions are must always be avoided because debugging them is a real pain. My vote is to do nothing and push the merge button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to agree with @marcosdiez here, if eliminating a race condition means that the code will be a bit uglier, I can live with that, since it will be really hard to debug something like "Sometimes catt doesn't work" or "catt doesn't work on my new Chromecast".
I would, however, like to have comments here and on CastStatusListener
explaining exactly which race condition this guards against, and how it does that. I can definitely see us coming back in a year, not remembering this discussion and saying "Oh we don't actually need this, we can just clear the app_ready
flag" and fall into the race condition trap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it looks like your suggestion sets the app_ready
flag if the app is not DashCast
, but don't we need a way to tell that DashCast is ready? Sorry to be such a pain, I just don't understand the flow here exactly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, but he also implemented the extra status text check, and it is the necessity of this I am contesting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@theychx Can we discuss somewhere more real-time? I've created a Gitter room and invited you there (you can just log in with Github).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcosdiez You're welcome too, of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @marcosdiez, @theychx and I discussed this on Gitter, and we agree that eliminating the race condition makes the most sense. However, could you change the code in the check to this?:
def _is_app_ready(self, status):
if status.app_id == self.app_id == DASHCAST_APP_ID:
# Big-ass comment about why this is here here.
return status.status_text == "Application ready"
return status.app_id == self.app_id
That way, we get rid of the confusingly-named class variable that's only used in one place, and we have a comment on the special case explaining exactly why it happens.
Also, could you add a comment on the check in __init__
on the CastStatusListener
about why the DashCast check is necessary? I think this is a good compromise between clearly documenting the special-case code and eliminating any potential race conditions.
catt/controllers.py
Outdated
# 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.start_app(self._cast_listener.app_id) | ||
self._cast_listener.app_ready.wait() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to agree with @marcosdiez here, if eliminating a race condition means that the code will be a bit uglier, I can live with that, since it will be really hard to debug something like "Sometimes catt doesn't work" or "catt doesn't work on my new Chromecast".
I would, however, like to have comments here and on CastStatusListener
explaining exactly which race condition this guards against, and how it does that. I can definitely see us coming back in a year, not remembering this discussion and saying "Oh we don't actually need this, we can just clear the app_ready
flag" and fall into the race condition trap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcosdiez Thank you for this contribution! We can merge once you have adopted @skorokithakis final suggestion.
Hi guys, @theychx you were right, @skorokithakis Yet on the race condition topic, I did check on the I did describe the race condition in the code as best as I could. Feel free to request further changes or modify yourself. And by the way, the triple comparison was a nice trick that I was not aware of. Thanks for teaching me that one! |
Thank you, @marcosdiez! The comment looks good to me. However, you seem to have removed |
It's cool. He just quoted the wrong line, the code is correct. |
Ah alright, great. Are you good to merge this, @theychx? |
Yep |
Fantastic! There are a few conflicts, it seems, which is a bit odd since we didn't do much to master. Is either of you at a computer and able to resolve those? |
It was just the gitter badge. |
Thanks, I realized there was a button but it didn't show on mobile. |
Merged, thank you both! |
In another step to make
catt
cast ALL the things, here is a simple PR to make it cast any website to Chromecast.The heavy lifting is done by DashCast ( http://stestagg.github.io/dashcast/ ), which was easier than I thought to implement on
catt
becausepychromecast
already supports it.I decided not to change the UI by falling back to
dastcast
every timeyoutube-dl
can not handle a URL.That means you just needs to type:
catt cast https://www.github.com/
to see github on your chromecast!
catt info
shows the title of the webpage that is being displayed.