diff options
Diffstat (limited to 'src/github/daneren2005/dsub/service/ChromeCastController.java')
-rw-r--r-- | src/github/daneren2005/dsub/service/ChromeCastController.java | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/src/github/daneren2005/dsub/service/ChromeCastController.java b/src/github/daneren2005/dsub/service/ChromeCastController.java new file mode 100644 index 00000000..49f36997 --- /dev/null +++ b/src/github/daneren2005/dsub/service/ChromeCastController.java @@ -0,0 +1,345 @@ +/* + 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.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.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.GooglePlayServicesUtil; +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.IOException; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.CastCompat; + +/** + * 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 ConnectionCallbacks connectionCallbacks; + private ConnectionFailedListener connectionFailedListener; + private Cast.Listener castClientListener; + + private boolean applicationStarted = false; + private boolean waitingForReconnect = false; + private boolean error = false; + + private RemoteMediaPlayer mediaPlayer; + private double gain = 0.5; + + public ChromeCastController(DownloadServiceImpl downloadService, CastDevice castDevice) { + this.downloadService = downloadService; + this.castDevice = castDevice; + + connectionCallbacks = new ConnectionCallbacks(); + connectionFailedListener = new ConnectionFailedListener(); + castClientListener = new Cast.Listener() { + @Override + public void onApplicationStatusChanged() { + if (apiClient != null) { + Log.d(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(apiClient)); + } + } + + @Override + public void onVolumeChanged() { + if (apiClient != null) { + gain = Cast.CastApi.getVolume(apiClient); + } + } + + @Override + public void onApplicationDisconnected(int errorCode) { + shutdown(); + } + + }; + + Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, castClientListener); + apiClient = new GoogleApiClient.Builder(downloadService) + .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); + 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; + } + + @Override + public void updatePlaylist() { + + } + + @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); + } + + @Override + public void setVolume(boolean up) { + double delta = up ? 0.1 : -0.1; + gain += delta; + gain = Math.max(gain, 0.0); + gain = Math.min(gain, 1.0); + + getVolumeToast().setVolume((float) gain); + try { + Cast.CastApi.setVolume(apiClient, gain); + } catch(Exception e) { + Log.e(TAG, "Failed to the volume"); + } + } + + @Override + public int getRemotePosition() { + if(mediaPlayer != null) { + return (int) (mediaPlayer.getApproximateStreamPosition() / 1000L); + } else { + return 0; + } + } + + void startSong(DownloadFile currentPlaying, boolean autoStart) { + if(currentPlaying == null) { + // Don't start anything + return; + } + downloadService.setPlayerState(PlayerState.PREPARING); + MusicDirectory.Entry song = currentPlaying.getSong(); + + try { + MusicService musicService = MusicServiceFactory.getMusicService(downloadService); + String url = song.isVideo() ? musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService) : musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate()); + // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC + url = url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID); + + // 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()); + String coverArt = musicService.getCoverArtUrl(downloadService, song); + meta.addImage(new WebImage(Uri.parse(coverArt))); + } + + // Load it into a MediaInfo wrapper + MediaInfo mediaInfo = new MediaInfo.Builder(url) + .setContentType(song.isVideo() ? "application/x-mpegURL" : song.getTranscodedContentType()) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setMetadata(meta) + .build(); + + mediaPlayer.load(apiClient, mediaInfo, autoStart).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult result) { + if (result.getStatus().isSuccess()) { + if(mediaPlayer.getMediaStatus().getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING) { + downloadService.setPlayerState(PlayerState.STARTED); + } else { + downloadService.setPlayerState(PlayerState.PREPARED); + } + } else if(result.getStatus().getStatusCode() != ConnectionResult.SIGN_IN_REQUIRED) { + Log.e(TAG, "Failed to load: " + result.getStatus().toString()); + downloadService.setPlayerState(PlayerState.STOPPED); + error = true; + Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load)); + } + } + }); + } catch (IllegalStateException e) { + Log.e(TAG, "Problem occurred with media during loading", e); + } catch (Exception e) { + Log.e(TAG, "Problem opening media during loading", e); + } + } + + + private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { + @Override + public void onConnected(Bundle connectionHint) { + if (waitingForReconnect) { + waitingForReconnect = false; + // reconnectChannels(); + } else { + launchApplication(); + } + } + + @Override + public void onConnectionSuspended(int cause) { + waitingForReconnect = true; + } + + void launchApplication() { + try { + Cast.CastApi.launchApplication(apiClient, CastCompat.APPLICATION_ID, false).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() { + @Override + public void onResult(Cast.ApplicationConnectionResult result) { + Status status = result.getStatus(); + if (status.isSuccess()) { + ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); + String sessionId = result.getSessionId(); + String applicationStatus = result.getApplicationStatus(); + boolean wasLaunched = result.getWasLaunched(); + + applicationStarted = true; + setupChannel(); + } else { + shutdown(); + } + } + }); + } catch (Exception e) { + Log.e(TAG, "Failed to launch application", e); + } + } + void setupChannel() { + mediaPlayer = new RemoteMediaPlayer(); + mediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { + @Override + public void onStatusUpdated() { + MediaStatus mediaStatus = mediaPlayer.getMediaStatus(); + Log.d(TAG, "state: " + mediaStatus.getPlayerState()); + switch(mediaStatus.getPlayerState()) { + case MediaStatus.PLAYER_STATE_PLAYING: + downloadService.setPlayerState(PlayerState.STARTED); + break; + case MediaStatus.PLAYER_STATE_PAUSED: + downloadService.setPlayerState(PlayerState.PAUSED); + break; + case MediaStatus.PLAYER_STATE_BUFFERING: + downloadService.setPlayerState(PlayerState.PREPARING); + break; + case MediaStatus.PLAYER_STATE_IDLE: + downloadService.setPlayerState(PlayerState.COMPLETED); + downloadService.next(); + break; + } + } + }); + mediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() { + @Override + public void onMetadataUpdated() { + MediaInfo mediaInfo = mediaPlayer.getMediaInfo(); + // TODO: Do I care about this? + } + }); + + try { + Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); + } catch (IOException e) { + Log.e(TAG, "Exception while creating channel", e); + } + + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + startSong(currentPlaying, true); + } + } + + private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { + @Override + public void onConnectionFailed(ConnectionResult result) { + shutdown(); + } + } +} |