/* This file is part of Subsonic. Subsonic is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Subsonic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Subsonic. If not, see <http://www.gnu.org/licenses/>. Copyright 2014 (C) Scott Jackson */ package github.daneren2005.dsub.service; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.util.Log; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.RemoteMediaPlayer; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.images.WebImage; import java.io.File; import java.io.IOException; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RemoteControlState; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.EnvironmentVariables; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.util.compat.CastCompat; import github.daneren2005.serverproxy.FileProxy; import github.daneren2005.serverproxy.ServerProxy; import github.daneren2005.serverproxy.WebProxy; /** * Created by owner on 2/9/14. */ public class ChromeCastController extends RemoteController { private static final String TAG = ChromeCastController.class.getSimpleName(); private CastDevice castDevice; private GoogleApiClient apiClient; private boolean applicationStarted = false; private boolean waitingForReconnect = false; private boolean error = false; private boolean ignoreNextPaused = false; private String sessionId; private boolean isStopping = false; private Runnable afterUpdateComplete = null; private RemoteMediaPlayer mediaPlayer; private double gain = 0.5; public ChromeCastController(DownloadService downloadService, CastDevice castDevice) { super(downloadService); this.castDevice = castDevice; } @Override public void create(boolean playing, int seconds) { downloadService.setPlayerState(PlayerState.PREPARING); ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks(playing, seconds); ConnectionFailedListener connectionFailedListener = new ConnectionFailedListener(); Cast.Listener castClientListener = new Cast.Listener() { @Override public void onApplicationStatusChanged() { if (apiClient != null && apiClient.isConnected()) { Log.i(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(apiClient)); } } @Override public void onVolumeChanged() { if (apiClient != null && applicationStarted) { try { gain = Cast.CastApi.getVolume(apiClient); } catch (Exception e) { Log.w(TAG, "Failed to get volume"); } } } @Override public void onApplicationDisconnected(int errorCode) { shutdownInternal(); } }; Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, castClientListener).setVerboseLoggingEnabled(true); apiClient = new GoogleApiClient.Builder(downloadService).useDefaultAccount() .addApi(Cast.API, apiOptionsBuilder.build()) .addConnectionCallbacks(connectionCallbacks) .addOnConnectionFailedListener(connectionFailedListener) .build(); apiClient.connect(); } @Override public void start() { if(error) { error = false; Log.w(TAG, "Attempting to restart song"); startSong(downloadService.getCurrentPlaying(), true, 0); return; } try { mediaPlayer.play(apiClient); } catch(Exception e) { Log.e(TAG, "Failed to start"); } } @Override public void stop() { try { mediaPlayer.pause(apiClient); } catch(Exception e) { Log.e(TAG, "Failed to pause"); } } @Override public void shutdown() { try { if(mediaPlayer != null && !error) { mediaPlayer.stop(apiClient); } } catch(Exception e) { Log.e(TAG, "Failed to stop mediaPlayer", e); } try { if(apiClient != null) { Cast.CastApi.stopApplication(apiClient); Cast.CastApi.removeMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace()); mediaPlayer = null; applicationStarted = false; } } catch(Exception e) { Log.e(TAG, "Failed to shutdown application", e); } if(apiClient != null && apiClient.isConnected()) { apiClient.disconnect(); } apiClient = null; if(proxy != null) { proxy.stop(); proxy = null; } } private void shutdownInternal() { // This will call this.shutdown() indirectly downloadService.setRemoteEnabled(RemoteControlState.LOCAL, null); } @Override public void updatePlaylist() { if(downloadService.getCurrentPlaying() == null) { startSong(null, false, 0); } } @Override public void changePosition(int seconds) { try { mediaPlayer.seek(apiClient, seconds * 1000L); } catch(Exception e) { Log.e(TAG, "FAiled to seek to " + seconds); } } @Override public void changeTrack(int index, DownloadFile song) { startSong(song, true, 0); } @Override public void setVolume(int volume) { gain = volume / 10.0; try { Cast.CastApi.setVolume(apiClient, gain); } catch(Exception e) { Log.e(TAG, "Failed to the volume"); } } @Override public void updateVolume(boolean up) { double delta = up ? 0.1 : -0.1; gain += delta; gain = Math.max(gain, 0.0); gain = Math.min(gain, 1.0); try { Cast.CastApi.setVolume(apiClient, gain); } catch(Exception e) { Log.e(TAG, "Failed to the volume"); } } @Override public double getVolume() { return Cast.CastApi.getVolume(apiClient); } @Override public int getRemotePosition() { if(mediaPlayer != null) { return (int) (mediaPlayer.getApproximateStreamPosition() / 1000L); } else { return 0; } } @Override public int getRemoteDuration() { if(mediaPlayer != null) { return (int) (mediaPlayer.getStreamDuration() / 1000L); } else { return 0; } } void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) { if(currentPlaying == null) { try { if (mediaPlayer != null && !error && !isStopping) { isStopping = true; mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { @Override public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) { isStopping = false; if(afterUpdateComplete != null) { afterUpdateComplete.run(); afterUpdateComplete = null; } } }); } } catch(Exception e) { // Just means it didn't need to be stopped } downloadService.setPlayerState(PlayerState.IDLE); return; } else if(isStopping) { afterUpdateComplete = new Runnable() { @Override public void run() { startSong(currentPlaying, autoStart, position); } }; return; } downloadService.setPlayerState(PlayerState.PREPARING); MusicDirectory.Entry song = currentPlaying.getSong(); try { MusicService musicService = MusicServiceFactory.getMusicService(downloadService); String url = getStreamUrl(musicService, currentPlaying); // Setup song/video information MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK); meta.putString(MediaMetadata.KEY_TITLE, song.getTitle()); if(song.getTrack() != null) { meta.putInt(MediaMetadata.KEY_TRACK_NUMBER, song.getTrack()); } if(!song.isVideo()) { meta.putString(MediaMetadata.KEY_ARTIST, song.getArtist()); meta.putString(MediaMetadata.KEY_ALBUM_ARTIST, song.getArtist()); meta.putString(MediaMetadata.KEY_ALBUM_TITLE, song.getAlbum()); if(castDevice.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT)) { if (proxy == null || proxy instanceof WebProxy) { String coverArt = musicService.getCoverArtUrl(downloadService, song); // If proxy is going, it is a web proxy if (proxy != null) { coverArt = proxy.getPublicAddress(coverArt); } meta.addImage(new WebImage(Uri.parse(coverArt))); } else { File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song); if (coverArtFile != null && coverArtFile.exists()) { String coverArt = proxy.getPublicAddress(coverArtFile.getPath()); meta.addImage(new WebImage(Uri.parse(coverArt))); } } } } String contentType; if(song.isVideo()) { contentType = "application/x-mpegURL"; } else if(song.getTranscodedContentType() != null) { contentType = song.getTranscodedContentType(); } else if(song.getContentType() != null) { contentType = song.getContentType(); } else { contentType = "audio/mpeg"; } // Load it into a MediaInfo wrapper MediaInfo mediaInfo = new MediaInfo.Builder(url) .setContentType(contentType) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setMetadata(meta) .build(); if(autoStart) { ignoreNextPaused = true; } ResultCallback callback = new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { @Override public void onResult(RemoteMediaPlayer.MediaChannelResult result) { if (result.getStatus().isSuccess()) { // Handled in other handler } else if(result.getStatus().getStatusCode() == CastStatusCodes.REPLACED) { Log.w(TAG, "Request was replaced: " + currentPlaying.toString()); } else { Log.e(TAG, "Failed to load: " + result.getStatus().toString()); failedLoad(); } } }; if(position > 0) { mediaPlayer.load(apiClient, mediaInfo, autoStart, position * 1000L).setResultCallback(callback); } else { mediaPlayer.load(apiClient, mediaInfo, autoStart).setResultCallback(callback); } } catch (IllegalStateException e) { Log.e(TAG, "Problem occurred with media during loading", e); failedLoad(); } catch (Exception e) { Log.e(TAG, "Problem opening media during loading", e); failedLoad(); } } private void failedLoad() { Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load)); downloadService.setPlayerState(PlayerState.STOPPED); error = true; } private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { private boolean isPlaying; private int position; private ResultCallback<Cast.ApplicationConnectionResult> resultCallback; ConnectionCallbacks(boolean isPlaying, int position) { this.isPlaying = isPlaying; this.position = position; resultCallback = new ResultCallback<Cast.ApplicationConnectionResult>() { @Override public void onResult(Cast.ApplicationConnectionResult result) { Status status = result.getStatus(); if (status.isSuccess()) { ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); sessionId = result.getSessionId(); String applicationStatus = result.getApplicationStatus(); boolean wasLaunched = result.getWasLaunched(); applicationStarted = true; setupChannel(); } else { shutdownInternal(); } } }; } @Override public void onConnected(Bundle connectionHint) { if (waitingForReconnect) { Log.i(TAG, "Reconnecting"); reconnectApplication(); } else { launchApplication(); } } @Override public void onConnectionSuspended(int cause) { Log.w(TAG, "Connection suspended"); isPlaying = downloadService.getPlayerState() == PlayerState.STARTED; position = getRemotePosition(); waitingForReconnect = true; } void launchApplication() { try { Cast.CastApi.launchApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, false).setResultCallback(resultCallback); } catch (Exception e) { Log.e(TAG, "Failed to launch application", e); } } void reconnectApplication() { try { Cast.CastApi.joinApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, sessionId).setResultCallback(resultCallback); } catch (Exception e) { Log.e(TAG, "Failed to reconnect application", e); } } void setupChannel() { if(!waitingForReconnect) { mediaPlayer = new RemoteMediaPlayer(); mediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { @Override public void onStatusUpdated() { MediaStatus mediaStatus = mediaPlayer.getMediaStatus(); if (mediaStatus == null) { return; } switch (mediaStatus.getPlayerState()) { case MediaStatus.PLAYER_STATE_PLAYING: if (ignoreNextPaused) { ignoreNextPaused = false; } downloadService.setPlayerState(PlayerState.STARTED); break; case MediaStatus.PLAYER_STATE_PAUSED: if (!ignoreNextPaused) { downloadService.setPlayerState(PlayerState.PAUSED); } break; case MediaStatus.PLAYER_STATE_BUFFERING: downloadService.setPlayerState(PlayerState.PREPARING); break; case MediaStatus.PLAYER_STATE_IDLE: if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) { if(downloadService.getPlayerState() != PlayerState.PREPARING) { downloadService.onSongCompleted(); } } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) { if (downloadService.getPlayerState() != PlayerState.PREPARING) { downloadService.setPlayerState(PlayerState.PREPARING); } } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_ERROR) { Log.e(TAG, "Idle due to unknown error"); downloadService.onSongCompleted(); } else { Log.w(TAG, "Idle reason: " + mediaStatus.getIdleReason()); downloadService.setPlayerState(PlayerState.IDLE); } break; } } }); } try { Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); } catch (Exception e) { Log.e(TAG, "Exception while creating channel", e); } if(!waitingForReconnect) { DownloadFile currentPlaying = downloadService.getCurrentPlaying(); startSong(currentPlaying, isPlaying, position); } if(waitingForReconnect) { waitingForReconnect = false; } } } private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { @Override public void onConnectionFailed(ConnectionResult result) { shutdownInternal(); } } }