aboutsummaryrefslogtreecommitdiff
path: root/src/github/daneren2005
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2014-02-15 14:24:09 -0800
committerScott Jackson <daneren2005@gmail.com>2014-02-15 14:24:09 -0800
commit497ae37bae7a70c39aa96bc72a6f5259e6698070 (patch)
tree7c4a28463cc4a022d882380bf61fbf34b8866a33 /src/github/daneren2005
parentb28cf73fdf15b5fe47382b0832fa69b7070e14b6 (diff)
parentabe7f243dee1b85a0f82fab3fe552c8ae4e6ee87 (diff)
downloaddsub-497ae37bae7a70c39aa96bc72a6f5259e6698070.tar.gz
dsub-497ae37bae7a70c39aa96bc72a6f5259e6698070.tar.bz2
dsub-497ae37bae7a70c39aa96bc72a6f5259e6698070.zip
Merge branch 'Chromecast' of https://github.com/daneren2005/Subsonic
Diffstat (limited to 'src/github/daneren2005')
-rw-r--r--src/github/daneren2005/dsub/fragments/DownloadFragment.java31
-rw-r--r--src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java106
-rw-r--r--src/github/daneren2005/dsub/service/CachedMusicService.java14
-rw-r--r--src/github/daneren2005/dsub/service/ChromeCastController.java345
-rw-r--r--src/github/daneren2005/dsub/service/DownloadService.java9
-rw-r--r--src/github/daneren2005/dsub/service/DownloadServiceImpl.java38
-rw-r--r--src/github/daneren2005/dsub/service/MusicService.java4
-rw-r--r--src/github/daneren2005/dsub/service/RESTMusicService.java21
-rw-r--r--src/github/daneren2005/dsub/util/Constants.java1
-rw-r--r--src/github/daneren2005/dsub/util/MediaRouteManager.java93
-rw-r--r--src/github/daneren2005/dsub/util/compat/CastCompat.java57
11 files changed, 694 insertions, 25 deletions
diff --git a/src/github/daneren2005/dsub/fragments/DownloadFragment.java b/src/github/daneren2005/dsub/fragments/DownloadFragment.java
index d7f58e88..d87d8432 100644
--- a/src/github/daneren2005/dsub/fragments/DownloadFragment.java
+++ b/src/github/daneren2005/dsub/fragments/DownloadFragment.java
@@ -13,6 +13,7 @@ import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
+import android.support.v7.app.MediaRouteButton;
import android.view.ContextMenu;
import android.view.Display;
import android.view.GestureDetector;
@@ -41,7 +42,6 @@ import github.daneren2005.dsub.R;
import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
-import github.daneren2005.dsub.domain.RemoteControlState;
import github.daneren2005.dsub.domain.RepeatMode;
import github.daneren2005.dsub.service.DownloadFile;
import github.daneren2005.dsub.service.DownloadService;
@@ -88,7 +88,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
private ImageButton repeatButton;
private Button equalizerButton;
private Button visualizerButton;
- private Button jukeboxButton;
private View toggleListButton;
private ImageButton starButton;
private ImageButton bookmarkButton;
@@ -170,7 +169,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
repeatButton = (ImageButton)rootView.findViewById(R.id.download_repeat);
equalizerButton = (Button)rootView.findViewById(R.id.download_equalizer);
visualizerButton = (Button)rootView.findViewById(R.id.download_visualizer);
- jukeboxButton = (Button)rootView.findViewById(R.id.download_jukebox);
bookmarkButton = (ImageButton) rootView.findViewById(R.id.download_bookmark);
LinearLayout visualizerViewLayout = (LinearLayout)rootView.findViewById(R.id.download_visualizer_view_layout);
toggleListButton =rootView.findViewById(R.id.download_toggle_list);
@@ -203,7 +201,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
startButton.setOnTouchListener(touchListener);
equalizerButton.setOnTouchListener(touchListener);
visualizerButton.setOnTouchListener(touchListener);
- jukeboxButton.setOnTouchListener(touchListener);
bookmarkButton.setOnTouchListener(touchListener);
emptyTextView.setOnTouchListener(touchListener);
albumArtImageView.setOnTouchListener(touchListener);
@@ -375,17 +372,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
}
});
- jukeboxButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- boolean jukeboxEnabled = !getDownloadService().isRemoteEnabled();
- getDownloadService().setRemoteEnabled(jukeboxEnabled ? RemoteControlState.JUKEBOX_SERVER : RemoteControlState.LOCAL);
- updateButtons();
- Util.toast(context, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
- setControlsVisible(true);
- }
- });
-
bookmarkButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -516,6 +502,11 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
if(downloadService != null && downloadService.getKeepScreenOn() && nowPlaying) {
menu.findItem(R.id.menu_screen_on_off).setTitle(R.string.download_menu_screen_off);
}
+ if(downloadService != null) {
+ MenuItem mediaRouteItem = menu.findItem(R.id.menu_mediaroute);
+ MediaRouteButton mediaRouteButton = (MediaRouteButton) mediaRouteItem.getActionView();
+ mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector());
+ }
}
@Override
@@ -758,6 +749,9 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
if(currentPlaying == null && downloadService != null && currentPlaying == downloadService.getCurrentPlaying()) {
getImageLoader().loadImage(albumArtImageView, null, true, false);
}
+ if(downloadService != null) {
+ downloadService.startRemoteScan();
+ }
}
@Override
@@ -767,6 +761,9 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
if (visualizerView != null && visualizerView.isActive()) {
visualizerView.setActive(false);
}
+ if(getDownloadService() != null) {
+ getDownloadService().stopRemoteScan();
+ }
}
@Override
@@ -832,9 +829,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
if (visualizerView != null) {
visualizerButton.setTextColor(visualizerView.isActive() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
}
-
- boolean jukeboxEnabled = getDownloadService() != null && getDownloadService().isRemoteEnabled();
- jukeboxButton.setTextColor(jukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
if(Util.isOffline(context)) {
bookmarkButton.setVisibility(View.GONE);
@@ -1195,7 +1189,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe
break;
}
- jukeboxButton.setTextColor(isJukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
onProgressChangedTask = null;
}
};
diff --git a/src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java b/src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java
new file mode 100644
index 00000000..eaefbf55
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java
@@ -0,0 +1,106 @@
+/*
+ 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.provider;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DownloadService;
+
+/**
+ * Created by Scott on 11/28/13.
+ */
+public class JukeboxRouteProvider extends MediaRouteProvider {
+ public static final String CATEGORY_SAMPLE_ROUTE = "github.daneren2005.dsub.SERVER_JUKEBOX";
+ private static int MAX_VOLUME = 10;
+
+ private DownloadService downloadService;
+
+ public JukeboxRouteProvider(Context context) {
+ super(context);
+ this.downloadService = (DownloadService) context;
+
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create route descriptor
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder("Jukebox Route", "Subsonic Jukebox");
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription("Subsonic Jukebox")
+ .setVolume(5)
+ .setVolumeMax(MAX_VOLUME)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+ providerBuilder.addRoute(routeBuilder.build());
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public MediaRouteProvider.RouteController onCreateRouteController(String routeId) {
+ return new JukeboxRouteController(downloadService);
+ }
+
+ private static class JukeboxRouteController extends RouteController {
+ private DownloadService downloadService;
+
+ public JukeboxRouteController(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ }
+
+ @Override
+ public void onSelect() {
+ downloadService.setRemoteEnabled(RemoteControlState.JUKEBOX_SERVER);
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/CachedMusicService.java b/src/github/daneren2005/dsub/service/CachedMusicService.java
index b8b440d7..0826e967 100644
--- a/src/github/daneren2005/dsub/service/CachedMusicService.java
+++ b/src/github/daneren2005/dsub/service/CachedMusicService.java
@@ -283,7 +283,12 @@ public class CachedMusicService implements MusicService {
return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener);
}
- @Override
+ @Override
+ public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception {
+ return musicService.getCoverArtUrl(context, entry);
+ }
+
+ @Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener) throws Exception {
return musicService.getCoverArt(context, entry, size, progressListener);
}
@@ -293,7 +298,12 @@ public class CachedMusicService implements MusicService {
return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
}
- @Override
+ @Override
+ public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception {
+ return musicService.getMusicUrl(context, song, maxBitrate);
+ }
+
+ @Override
public Version getLocalVersion(Context context) throws Exception {
return musicService.getLocalVersion(context);
}
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();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java
index 1a254c73..471b6d5d 100644
--- a/src/github/daneren2005/dsub/service/DownloadService.java
+++ b/src/github/daneren2005/dsub/service/DownloadService.java
@@ -18,6 +18,8 @@
*/
package github.daneren2005.dsub.service;
+import android.support.v7.media.MediaRouteSelector;
+
import java.util.List;
import github.daneren2005.dsub.audiofx.EqualizerController;
@@ -27,6 +29,7 @@ import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
import github.daneren2005.dsub.domain.RemoteControlState;
import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.util.MediaRouteManager;
/**
* @author Sindre Mehus
@@ -124,11 +127,17 @@ public interface DownloadService {
VisualizerController getVisualizerController();
+ MediaRouteSelector getRemoteSelector();
+
boolean isRemoteEnabled();
void setRemoteEnabled(RemoteControlState newState);
void setRemoteVolume(boolean up);
+
+ void startRemoteScan();
+
+ void stopRemoteScan();
void setSleepTimerDuration(int duration);
diff --git a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
index 9f1e5ee9..e83be019 100644
--- a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
+++ b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
@@ -37,6 +37,7 @@ import github.daneren2005.dsub.domain.RepeatMode;
import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
import github.daneren2005.dsub.util.CancellableTask;
import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.MediaRouteManager;
import github.daneren2005.dsub.util.ShufflePlayBuffer;
import github.daneren2005.dsub.util.SimpleServiceBinder;
import github.daneren2005.dsub.util.Util;
@@ -63,6 +64,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
+import android.support.v7.media.MediaRouteSelector;
import android.util.Log;
import android.support.v4.util.LruCache;
import java.net.URLEncoder;
@@ -136,6 +138,8 @@ public class DownloadServiceImpl extends Service implements DownloadService {
private int timerDuration;
private boolean autoPlayStart = false;
+ private MediaRouteManager mediaRouter;
+
static {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
equalizerAvailable = true;
@@ -210,6 +214,8 @@ public class DownloadServiceImpl extends Service implements DownloadService {
getVisualizerController();
showVisualization = true;
}
+
+ mediaRouter = new MediaRouteManager(this);
}
@Override
@@ -366,7 +372,7 @@ public class DownloadServiceImpl extends Service implements DownloadService {
SharedPreferences prefs = Util.getPreferences(this);
remoteState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)];
if(remoteState != RemoteControlState.LOCAL) {
- setRemoteState(remoteState);
+ setRemoteState(remoteState, null);
}
boolean startShufflePlay = prefs.getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, false);
download(songs, false, false, false, false);
@@ -1118,19 +1124,27 @@ public class DownloadServiceImpl extends Service implements DownloadService {
}
@Override
+ public MediaRouteSelector getRemoteSelector() {
+ return mediaRouter.getSelector();
+ }
+
+ @Override
public boolean isRemoteEnabled() {
return remoteState != RemoteControlState.LOCAL;
}
@Override
public void setRemoteEnabled(RemoteControlState newState) {
- setRemoteState(newState);
+ setRemoteEnabled(newState, null);
+ }
+ public void setRemoteEnabled(RemoteControlState newState, Object ref) {
+ setRemoteState(newState, ref);
SharedPreferences.Editor editor = Util.getPreferences(this).edit();
editor.putInt(Constants.PREFERENCES_KEY_CONTROL_MODE, newState.getValue());
editor.commit();
}
- private void setRemoteState(RemoteControlState newState) {
+ private void setRemoteState(RemoteControlState newState, Object ref) {
if(remoteController != null) {
remoteController.stop();
setPlayerState(PlayerState.IDLE);
@@ -1143,6 +1157,14 @@ public class DownloadServiceImpl extends Service implements DownloadService {
case JUKEBOX_SERVER:
remoteController = new JukeboxController(this, handler);
break;
+ case CHROMECAST:
+ // TODO: Fix case where starting up with chromecast set
+ if(ref == null) {
+ remoteState = RemoteControlState.LOCAL;
+ break;
+ }
+ remoteController = (RemoteController) ref;
+ break;
case LOCAL: default:
break;
}
@@ -1169,6 +1191,16 @@ public class DownloadServiceImpl extends Service implements DownloadService {
remoteController.setVolume(up);
}
+ @Override
+ public void startRemoteScan() {
+ mediaRouter.startScan();
+ }
+
+ @Override
+ public void stopRemoteScan() {
+ mediaRouter.stopScan();
+ }
+
private synchronized void bufferAndPlay() {
bufferAndPlay(0);
}
diff --git a/src/github/daneren2005/dsub/service/MusicService.java b/src/github/daneren2005/dsub/service/MusicService.java
index 3674fd01..d5fdc251 100644
--- a/src/github/daneren2005/dsub/service/MusicService.java
+++ b/src/github/daneren2005/dsub/service/MusicService.java
@@ -91,10 +91,14 @@ public interface MusicService {
MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception;
+ String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception;
+
Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener) throws Exception;
HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception;
+ String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception;
+
Version getLocalVersion(Context context) throws Exception;
Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception;
diff --git a/src/github/daneren2005/dsub/service/RESTMusicService.java b/src/github/daneren2005/dsub/service/RESTMusicService.java
index 43b5f887..37e2bf44 100644
--- a/src/github/daneren2005/dsub/service/RESTMusicService.java
+++ b/src/github/daneren2005/dsub/service/RESTMusicService.java
@@ -644,6 +644,14 @@ public class RESTMusicService implements MusicService {
}
}
+ @Override
+ public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception {
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "getCoverArt", false));
+ builder.append("&id=").append(entry.getId());
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ return url;
+ }
+
@Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener) throws Exception {
@@ -723,7 +731,18 @@ public class RESTMusicService implements MusicService {
return response;
}
- @Override
+ @Override
+ public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception {
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "stream", false));
+ builder.append("&id=").append(song.getId());
+ builder.append("&maxBitRate=").append(maxBitrate);
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using music URL: " + url);
+ return url;
+ }
+
+ @Override
public String getVideoUrl(int maxBitrate, Context context, String id) {
StringBuilder builder = new StringBuilder(getRestUrl(context, "videoPlayer"));
builder.append("&id=").append(id);
diff --git a/src/github/daneren2005/dsub/util/Constants.java b/src/github/daneren2005/dsub/util/Constants.java
index 5d80a961..2b96ac4c 100644
--- a/src/github/daneren2005/dsub/util/Constants.java
+++ b/src/github/daneren2005/dsub/util/Constants.java
@@ -31,6 +31,7 @@ public final class Constants {
// Note: Keep it as low as possible to maintain compatibility with older servers.
public static final String REST_PROTOCOL_VERSION = "1.2.0";
public static final String REST_CLIENT_ID = "DSub";
+ public static final String CHROMECAST_CLIENT_ID = "DSubCC";
public static final String LAST_VERSION = "subsonic.version";
// Names for intent extras.
diff --git a/src/github/daneren2005/dsub/util/MediaRouteManager.java b/src/github/daneren2005/dsub/util/MediaRouteManager.java
new file mode 100644
index 00000000..7c0d33e6
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/MediaRouteManager.java
@@ -0,0 +1,93 @@
+/*
+ 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.util;
+
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.provider.JukeboxRouteProvider;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.RemoteController;
+import github.daneren2005.dsub.util.compat.CastCompat;
+
+/**
+ * Created by owner on 2/8/14.
+ */
+public class MediaRouteManager extends MediaRouter.Callback {
+ private static final String TAG = MediaRouteManager.class.getSimpleName();
+ private static boolean castAvailable = false;
+
+ private DownloadServiceImpl downloadService;
+ private MediaRouter router;
+ private MediaRouteSelector selector;
+
+ static {
+ try {
+ CastCompat.checkAvailable();
+ castAvailable = true;
+ } catch(Throwable t) {
+ castAvailable = false;
+ }
+ }
+
+ public MediaRouteManager(DownloadServiceImpl downloadService) {
+ this.downloadService = downloadService;
+ router = MediaRouter.getInstance(downloadService);
+ addProviders();
+ buildSelector();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) {
+ if(castAvailable) {
+ RemoteController controller = CastCompat.getController(downloadService, info);
+ if(controller != null) {
+ downloadService.setRemoteEnabled(RemoteControlState.CHROMECAST, controller);
+ }
+ }
+ }
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ }
+
+ public void startScan() {
+ router.addCallback(selector, this, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+ public void stopScan() {
+ router.removeCallback(this);
+ }
+
+ public MediaRouteSelector getSelector() {
+ return selector;
+ }
+
+ private void addProviders() {
+ JukeboxRouteProvider routeProvider = new JukeboxRouteProvider(downloadService);
+ router.addProvider(routeProvider);
+ }
+ private void buildSelector() {
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ if(castAvailable) {
+ builder.addControlCategory(CastCompat.getCastControlCategory());
+ }
+ selector = builder.build();
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/compat/CastCompat.java b/src/github/daneren2005/dsub/util/compat/CastCompat.java
new file mode 100644
index 00000000..31581816
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/compat/CastCompat.java
@@ -0,0 +1,57 @@
+/*
+ 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.util.compat;
+
+import android.support.v7.media.MediaRouter;
+
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import github.daneren2005.dsub.service.ChromeCastController;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.RemoteController;
+
+/**
+ * Created by owner on 2/9/14.
+ */
+public final class CastCompat {
+ public static final String APPLICATION_ID = "5F85EBEB";
+
+ static {
+ try {
+ Class.forName("com.google.android.gms.cast.CastDevice");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public static RemoteController getController(DownloadServiceImpl downloadService, MediaRouter.RouteInfo info) {
+ CastDevice device = CastDevice.getFromBundle(info.getExtras());
+ if(device != null) {
+ return new ChromeCastController(downloadService, device);
+ } else {
+ return null;
+ }
+ }
+
+ public static String getCastControlCategory() {
+ return CastMediaControlIntent.categoryForCast(APPLICATION_ID);
+ }
+}