home-assistant-libs / pychromecast

Library for Python 3 to communicate with the Google Chromecast.
MIT License
2.52k stars 378 forks source link

Playback Stops in the Middle of Streaming #175

Open jhamilt5 opened 7 years ago

jhamilt5 commented 7 years ago

I'm writing an application that leverages both pychromecast and gmusicapi to create a collaborative music queue to stream to my Google Home. I send streaming URLs to my Google Home like so:

from gmusicapi import Mobileclient
import pychromecast
from getpass import getpass

email = input("Email: ")
pwd = getpass()
android_id = 'XXXXXXXXXXXXXXXX' # ID of registered google play music device
api = Mobileclient()
api.login(email, pwd, android_id)    # successful login

cast = pychromecast.Chromecast('192.168.10.104')
mc = cast.media_controller

track_id = 'Tpa22h67vzxazrkpbtmuu543h24'   # The Google Play Store ID for a song
stream_url = api.get_stream_url(track_id, device_id=android_id)   # yields a url pointing to a streaming source for an mp3
mc.play_media(stream_url, 'audio/mpeg')

My Google Home chimes, letting me know that a new app has started, and shortly after, the music begins playing. However, around 75% of the way through the song, playback stops, and it cannot be restarted with successive calls to play_media() or with voice commands.

According to this issue on gmusicapi it appears to be happening because the stream_url is only valid for ~90 seconds, and the song hasn't completely buffered.

Is it possible in play_media() to force a complete load of the song instead of a streamed buffer?

anthonyzhub commented 6 years ago

Hi,

I am having the same issue as well. However, everything works fine on a chromecast.

stefan-sherwood commented 5 years ago

This just started happening to me about a week ago with untouched code that has worked for years, almost exactly as shown above except that I'm using pychromecast.get_chromecasts() instead of explicitly providing the IP. It happens 100% of the time (previously never happened).

gallmerci commented 4 years ago

Same issue for me. The code was working long time but the music stops randomly. Does anyone already have a workaround or an idea how to debug this?

stefan-sherwood commented 4 years ago

I solved it by downloading the whole file and then playing from the local version. Because the download finishes before Google expires the link, it doesn't get cut off. A bit of a nuisance but it does work.

gallmerci commented 4 years ago

Thanks. But not really a satisfying solution :(

stefan-sherwood commented 4 years ago

It means that my code behaves as it did before Google broke it so I'm ok with it.

gallmerci commented 4 years ago

Good point. I implemented now a logic in my server application that tries to identify this situation by checking player state and idle reason. Then I just get a fresh URL and restart the song at the same position when it stopped playing. Of course, you hear the pause but it is just for 1-2 secs and so far I am quite satisfied with this workaround :)

anthonyzhub commented 4 years ago

@gallmerci Great job!

@stefan-sherwood You downloaded the file? I have some songs on my computer that I use for testing, but Google Home automatically stops a song after ~10 seconds. I am aware that the link expires before the song finishes, so how were you able to overcome this problem?

stefan-sherwood commented 4 years ago

I'm using Python 3.7 on Windows 10. I stripped out a bunch of error handling to make it easier to read.

self.cache_and_stream_gmusic_track_on_chromecast( track_id )


# Serve locally cached songs using a subclassed BaseHTTPRequestHandler:
def do_GET( self ):
    track_id = self.get_track_id_from_url( self.path )
    file = self.get_music_fullpath( track_id )

    seek_to = 0
    if "Range" in self.headers:
        res = re.match( "(?P<units>[^=]*)=(?P<start>\\d+)-(?P<end>\\d*)", self.headers[ "Range" ] )
        if res:
            seek_to = int( res.group( "start" ) )
            self.logger.log( f"Requested byte offset: {seek_to}" )

    with open( file, 'rb' ) as f:
        if seek_to:
            print( f"Seeking to {seek_to}" )
            f.seek( seek_to )
        data = f.read()

    self.send_response( 200 )
    self.send_header( "Accept-Ranges", "bytes" )
    self.send_header( "Content-Type", "audio/mpeg" ) 
    self.send_header( "Content-Disposition", f"filename={file} )
    self.send_header( "Content-Length", f"{len( data )}" )

# Connect to Google Music API
def get_music_api( self ):
    self.music = gmusicapi.Mobileclient( debug_logging = False )
    if not music.oauth_login( device_id = music.FROM_MAC_ADDRESS ):
        self.logger.err( "GMusic login failed" )

def get_music_url( self, track_id ):
    return f"{self.server_ip}/{self.get_cache_path( track_id )}"

def get_music_fullpath( self, track_id ):
    return f"{self.library_loc}\\{self.get_cache_path( track_id )}"

# Retrieve and cache a song:
def get_cache_path( self, track_id ):
    from urllib.request import urlretrieve

    url = self.music.get_stream_url( track_id )
    path = f"library\\{store_id}"
    thing = urlretrieve( url, path )

    if thing[1].get_content_maintype() == 'audio':
        self.db.add_song_to_cache( store_id, thing[0] )
        return path
    else:
        self.logger.error( Fetch of Song ID {store_id} returned a non-audio type")

def get_stream_target( self, device_name ):
    import pychromecast
    devices = pychromecast.get_chromecasts()
    return next( (dev for dev in devices if dev.name == device_name), None )

def cache_and_stream_gmusic_track_on_chromecast( self, song_id ):
    url = self.get_music_url( song_id )
    self.streamer = self.get_stream_target( 'Whole House' )
    self.streamer.media_controller.play_media( song_url, 'audio/mpeg', metadata = song_info, current_time=offset )