From d29f9f7073c4a74c072cf7894b8314e793cea9ac Mon Sep 17 00:00:00 2001 From: Scott Jackson Date: Wed, 11 Mar 2015 17:25:04 -0700 Subject: #466 Added support for next playing in RemoteController, implement for DLNA players that support SetNextAvTransportURI --- .../daneren2005/dsub/service/DLNAController.java | 317 +++++++++++++-------- .../daneren2005/dsub/service/DownloadService.java | 53 +++- .../daneren2005/dsub/service/RemoteController.java | 8 + 3 files changed, 251 insertions(+), 127 deletions(-) (limited to 'src') diff --git a/src/github/daneren2005/dsub/service/DLNAController.java b/src/github/daneren2005/dsub/service/DLNAController.java index 5daf0910..036b111e 100644 --- a/src/github/daneren2005/dsub/service/DLNAController.java +++ b/src/github/daneren2005/dsub/service/DLNAController.java @@ -19,6 +19,7 @@ import android.content.SharedPreferences; import android.os.Looper; import android.util.Log; +import org.fourthline.cling.controlpoint.ActionCallback; import org.fourthline.cling.controlpoint.ControlPoint; import org.fourthline.cling.controlpoint.SubscriptionCallback; import org.fourthline.cling.model.action.ActionInvocation; @@ -30,6 +31,7 @@ import org.fourthline.cling.model.meta.Service; import org.fourthline.cling.model.meta.StateVariable; import org.fourthline.cling.model.state.StateVariableValue; import org.fourthline.cling.model.types.ServiceType; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; import org.fourthline.cling.support.avtransport.callback.GetPositionInfo; import org.fourthline.cling.support.avtransport.callback.Pause; import org.fourthline.cling.support.avtransport.callback.Play; @@ -65,6 +67,7 @@ import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.Util; import github.daneren2005.serverproxy.FileProxy; import github.daneren2005.serverproxy.ServerProxy; @@ -79,6 +82,7 @@ public class DLNAController extends RemoteController { ControlPoint controlPoint; SubscriptionCallback callback; boolean supportsSeek = false; + boolean supportsSetupNext = false; private ServerProxy proxy; String rootLocation = ""; @@ -87,6 +91,8 @@ public class DLNAController extends RemoteController { final AtomicLong lastUpdate = new AtomicLong(); int currentPosition = 0; String currentPlayingURI; + String nextPlayingURI; + DownloadFile nextPlaying; boolean running = true; boolean hasDuration = false; Runnable searchDLNA = new Runnable() { @@ -108,6 +114,7 @@ public class DLNAController extends RemoteController { SharedPreferences prefs = Util.getPreferences(downloadService); rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + nextSupported = true; } @Override @@ -131,6 +138,10 @@ public class DLNAController extends RemoteController { } } } + Action setupNextAction = genaSubscription.getService().getAction("SetNextAVTransportURI"); + if(setupNextAction != null) { + supportsSetupNext = true; + } startSong(downloadService.getCurrentPlaying(), playing, seconds); downloadService.postDelayed(searchDLNA, SEARCH_UPDATE_INTERVAL_SECONDS); @@ -160,6 +171,11 @@ public class DLNAController extends RemoteController { switch (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class).getValue()) { case PLAYING: downloadService.setPlayerState(PlayerState.STARTED); + + // Try to setup next playing after playback start has been registered + if(supportsSetupNext && downloadService.getNextPlayerState() == PlayerState.IDLE) { + downloadService.setNextPlaying(); + } break; case PAUSED_PLAYBACK: downloadService.setPlayerState(PlayerState.PAUSED); @@ -307,6 +323,11 @@ public class DLNAController extends RemoteController { startSong(song, true, 0); } + @Override + public void changeNextTrack(DownloadFile song) { + setupNextSong(song); + } + @Override public void setVolume(int volume) { if(volume < 0) { @@ -382,152 +403,195 @@ public class DLNAController extends RemoteController { error = false; downloadService.setPlayerState(PlayerState.PREPARING); - MusicDirectory.Entry song = currentPlaying.getSong(); try { - // Get url for entry - MusicService musicService = MusicServiceFactory.getMusicService(downloadService); - String url; - // In offline mode or playing offline song - if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) { - if(proxy == null) { - proxy = new FileProxy(downloadService); - proxy.start(); - } + Pair songInfo = getSongInfo(currentPlaying); - // Offline song - if(song.getId().indexOf(rootLocation) != -1) { - url = proxy.getPublicAddress(song.getId()); - } else { - // Playing online song in offline mode - url = proxy.getPublicAddress(currentPlaying.getCompleteFile().getPath()); - } - } else { - // Check if we want a proxy going still - if(Util.isCastProxy(downloadService)) { - if(proxy instanceof FileProxy) { - proxy.stop(); - proxy = null; + currentPlayingURI = songInfo.getFirst(); + downloadService.setNextPlayerState(PlayerState.IDLE); + controlPoint.execute(new SetAVTransportURI(getTransportService(), songInfo.getFirst(), songInfo.getSecond()) { + @Override + public void success(ActionInvocation invocation) { + if(position != 0) { + changePosition(position); } - if(proxy == null) { - proxy = createWebProxy(); - proxy.start(); + if (autoStart) { + start(); + } else { + downloadService.setPlayerState(PlayerState.PAUSED); } - } else if(proxy != null) { - proxy.stop(); - proxy = null; - } - if(song.isVideo()) { - url = musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService); - } else { - url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate()); + currentPosition = position; + lastUpdate.set(System.currentTimeMillis()); + getUpdatedStatus(); } - // If proxy is going, it is a WebProxy - if(proxy != null) { - url = proxy.getPublicAddress(url); + @Override + public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) { + Log.w(TAG, "Set URI failed: " + msg); + failedLoad(); } - } + }); + } catch (Exception e) { + Log.w(TAG, "Failed startSong", e); + failedLoad(); + } + } + private void setupNextSong(final DownloadFile nextPlaying) { + this.nextPlaying = nextPlaying; + nextPlayingURI = null; + if(nextPlaying == null) { + downloadService.setNextPlayerState(PlayerState.IDLE); + Log.i(TAG, "Nothing to play next"); + return; + } - // Create metadata for entry - Item track; - if(song.isVideo()) { - track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist()); - } else { - String contentType = null; - if(song.getTranscodedContentType() != null) { - contentType = song.getTranscodedContentType(); - } else if(song.getContentType() != null) { - contentType = song.getContentType(); + downloadService.setNextPlayerState(PlayerState.PREPARING); + try { + Pair songInfo = getSongInfo(nextPlaying); + + nextPlayingURI = songInfo.getFirst(); + controlPoint.execute(new SetNextAVTransportURI(getTransportService(), songInfo.getFirst(), songInfo.getSecond()) { + @Override + public void success(ActionInvocation invocation) { + downloadService.setNextPlayerState(PlayerState.PREPARED); } - MimeType mimeType; - // If we can parse the content type, use it instead of hard coding - if(contentType != null && contentType.indexOf("/") != -1 && contentType.indexOf("/") != (contentType.length() - 1)) { - String[] typeParts = contentType.split("/"); - mimeType = new MimeType(typeParts[0], typeParts[1]); - } else { - mimeType = new MimeType("audio", "mpeg"); + @Override + public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) { + Log.w(TAG, "Set next URI failed: " + msg); + nextPlayingURI = null; + DLNAController.this.nextPlaying = null; + downloadService.setNextPlayerState(PlayerState.IDLE); } + }); + } catch (Exception e) { + Log.w(TAG, "Failed to setup next song", e); + nextPlayingURI = null; + this.nextPlaying = null; + downloadService.setNextPlayerState(PlayerState.IDLE); + } + } - Res res = new Res(mimeType, song.getSize(), url); + Pair getSongInfo(final DownloadFile downloadFile) throws Exception { + MusicDirectory.Entry song = downloadFile.getSong(); + + // Get url for entry + MusicService musicService = MusicServiceFactory.getMusicService(downloadService); + String url; + // In offline mode or playing offline song + if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) { + if(proxy == null) { + proxy = new FileProxy(downloadService); + proxy.start(); + } - if(song.getDuration() != null) { - SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); - df.setTimeZone(TimeZone.getTimeZone("UTC")); - res.setDuration(df.format(new Date(song.getDuration() * 1000))); + // Offline song + if(song.getId().indexOf(rootLocation) != -1) { + url = proxy.getPublicAddress(song.getId()); + } else { + // Playing online song in offline mode + url = proxy.getPublicAddress(downloadFile.getCompleteFile().getPath()); + } + } else { + // Check if we want a proxy going still + if(Util.isCastProxy(downloadService)) { + if(proxy instanceof FileProxy) { + proxy.stop(); + proxy = null; } - MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist(), res); - musicTrack.setOriginalTrackNumber(song.getTrack()); + if(proxy == null) { + proxy = createWebProxy(); + proxy.start(); + } + } else if(proxy != null) { + proxy.stop(); + proxy = null; + } - if(song.getCoverArt() != null) { - String coverArt = null; - if(proxy == null || proxy instanceof WebProxy) { - coverArt = musicService.getCoverArtUrl(downloadService, song); + if(song.isVideo()) { + url = musicService.getHlsUrl(song.getId(), downloadFile.getBitRate(), downloadService); + } else { + url = musicService.getMusicUrl(downloadService, song, downloadFile.getBitRate()); + } - // If proxy is going, it is a web proxy - if(proxy != null) { - coverArt = proxy.getPublicAddress(coverArt); - } - } else { - File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song); - if(coverArtFile != null && coverArtFile.exists()) { - coverArt = proxy.getPublicAddress(coverArtFile.getPath()); - } - } + // If proxy is going, it is a WebProxy + if(proxy != null) { + url = proxy.getPublicAddress(url); + } + } - if(coverArt != null) { - DIDLObject.Property.UPNP.ALBUM_ART_URI albumArtUri = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(coverArt)); - musicTrack.addProperty(albumArtUri); - } - } + // Create metadata for entry + Item track; + if(song.isVideo()) { + track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist()); + } else { + String contentType = null; + if(song.getTranscodedContentType() != null) { + contentType = song.getTranscodedContentType(); + } else if(song.getContentType() != null) { + contentType = song.getContentType(); + } - track = musicTrack; + MimeType mimeType; + // If we can parse the content type, use it instead of hard coding + if(contentType != null && contentType.indexOf("/") != -1 && contentType.indexOf("/") != (contentType.length() - 1)) { + String[] typeParts = contentType.split("/"); + mimeType = new MimeType(typeParts[0], typeParts[1]); + } else { + mimeType = new MimeType("audio", "mpeg"); } - DIDLParser parser = new DIDLParser(); - DIDLContent didl = new DIDLContent(); - didl.addItem(track); + Res res = new Res(mimeType, song.getSize(), url); - String metadata = ""; - try { - metadata = parser.generate(didl); - } catch(Exception e) { - Log.w(TAG, "Metadata generation failed", e); + if(song.getDuration() != null) { + SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + res.setDuration(df.format(new Date(song.getDuration() * 1000))); } - currentPlayingURI = url; - controlPoint.execute(new SetAVTransportURI(getTransportService(), url, metadata) { - @Override - public void success(ActionInvocation invocation) { - if(position != 0) { - changePosition(position); - } + MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist(), res); + musicTrack.setOriginalTrackNumber(song.getTrack()); - if (autoStart) { - start(); - } else { - downloadService.setPlayerState(PlayerState.PAUSED); - } + if(song.getCoverArt() != null) { + String coverArt = null; + if(proxy == null || proxy instanceof WebProxy) { + coverArt = musicService.getCoverArtUrl(downloadService, song); - currentPosition = position; - lastUpdate.set(System.currentTimeMillis()); - getUpdatedStatus(); + // If proxy is going, it is a web proxy + if(proxy != null) { + coverArt = proxy.getPublicAddress(coverArt); + } + } else { + File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song); + if(coverArtFile != null && coverArtFile.exists()) { + coverArt = proxy.getPublicAddress(coverArtFile.getPath()); + } } - @Override - public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) { - Log.w(TAG, "Set URI failed: " + msg); - failedLoad(); + if(coverArt != null) { + DIDLObject.Property.UPNP.ALBUM_ART_URI albumArtUri = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(coverArt)); + musicTrack.addProperty(albumArtUri); } - }); - } catch (Exception e) { - Log.w(TAG, "Failed startSong", e); - failedLoad(); + } + + track = musicTrack; + } + + DIDLParser parser = new DIDLParser(); + DIDLContent didl = new DIDLContent(); + didl.addItem(track); + + String metadata = ""; + try { + metadata = parser.generate(didl); + } catch(Exception e) { + Log.w(TAG, "Metadata generation failed", e); } + + return new Pair(url, metadata); } private void failedLoad() { @@ -572,6 +636,12 @@ public class DLNAController extends RemoteController { // Let's get the updated position currentPosition = (int) positionInfo.getTrackElapsedSeconds(); + if(positionInfo.getTrackURI() != null && positionInfo.getTrackURI().equals(nextPlayingURI) && downloadService.getNextPlayerState() == PlayerState.PREPARED) { + downloadService.setCurrentPlaying(nextPlaying, true); + downloadService.setPlayerState(PlayerState.STARTED); + downloadService.setNextPlaying(); + } + downloadService.postDelayed(new Runnable() { @Override public void run() { @@ -593,4 +663,25 @@ public class DLNAController extends RemoteController { } }); } + + private abstract class SetNextAVTransportURI extends ActionCallback { + public SetNextAVTransportURI(Service service, String uri) { + this(new UnsignedIntegerFourBytes(0), service, uri, null); + } + + public SetNextAVTransportURI(Service service, String uri, String metadata) { + this(new UnsignedIntegerFourBytes(0), service, uri, metadata); + } + + public SetNextAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri) { + this(instanceId, service, uri, null); + } + + public SetNextAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri, String metadata) { + super(new ActionInvocation(service.getAction("SetNextAVTransportURI"))); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("NextURI", uri); + getActionInvocation().setInput("NextURIMetaData", metadata); + } + } } diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java index 562d621c..a6b66d5a 100644 --- a/src/github/daneren2005/dsub/service/DownloadService.java +++ b/src/github/daneren2005/dsub/service/DownloadService.java @@ -757,11 +757,15 @@ public class DownloadService extends Service { synchronized void setNextPlaying() { SharedPreferences prefs = Util.getPreferences(DownloadService.this); - boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); - if(!gaplessPlayback) { - nextPlaying = null; - nextPlayerState = IDLE; - return; + + // Only obey gapless playback for local + if(remoteState == LOCAL) { + boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); + if (!gaplessPlayback) { + nextPlaying = null; + nextPlayerState = IDLE; + return; + } } setNextPlayerState(IDLE); @@ -774,10 +778,19 @@ public class DownloadService extends Service { } if(index < size() && index != -1 && index != currentPlayingIndex) { nextPlaying = downloadList.get(index); - nextPlayingTask = new CheckCompletionTask(nextPlaying); - nextPlayingTask.execute(); + + if(remoteState == LOCAL) { + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.execute(); + } else if(remoteController != null) { + remoteController.changeNextTrack(nextPlaying); + } } else { - resetNext(); + if(remoteState == LOCAL) { + resetNext(); + } else if(remoteController != null) { + remoteController.changeNextTrack(nextPlaying); + } nextPlaying = null; } } @@ -812,6 +825,10 @@ public class DownloadService extends Service { return currentDownloading; } + public DownloadFile getNextPlaying() { + return nextPlaying; + } + public List getSongs() { return downloadList; } @@ -1174,6 +1191,10 @@ public class DownloadService extends Service { return playerState; } + public PlayerState getNextPlayerState() { + return nextPlayerState; + } + public synchronized void setPlayerState(final PlayerState playerState) { Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); @@ -1277,7 +1298,7 @@ public class DownloadService extends Service { scrobbler.scrobble(this, currentPlaying, true); } - private synchronized void setNextPlayerState(PlayerState playerState) { + public synchronized void setNextPlayerState(PlayerState playerState) { Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")"); this.nextPlayerState = playerState; } @@ -1777,11 +1798,15 @@ public class DownloadService extends Service { DownloadFile movedSong = list.remove(from); list.add(to, movedSong); currentPlayingIndex = downloadList.indexOf(currentPlaying); - if(remoteState != LOCAL && mainList) { - updateRemotePlaylist(); - } else if(mainList && (movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to)) { - // Moving next playing, current playing, or moving a song to be next playing - setNextPlaying(); + if(mainList) { + if(remoteState == LOCAL || (remoteController != null && remoteController.isNextSupported())) { + // Moving next playing, current playing, or moving a song to be next playing + if(movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to) { + setNextPlaying(); + } + } else { + updateRemotePlaylist(); + } } } diff --git a/src/github/daneren2005/dsub/service/RemoteController.java b/src/github/daneren2005/dsub/service/RemoteController.java index 4483598c..cac28c09 100644 --- a/src/github/daneren2005/dsub/service/RemoteController.java +++ b/src/github/daneren2005/dsub/service/RemoteController.java @@ -37,6 +37,7 @@ import github.daneren2005.serverproxy.WebProxy; public abstract class RemoteController { private static final String TAG = RemoteController.class.getSimpleName(); protected DownloadService downloadService; + protected boolean nextSupported = false; public abstract void create(boolean playing, int seconds); public abstract void start(); @@ -46,6 +47,13 @@ public abstract class RemoteController { public abstract void updatePlaylist(); public abstract void changePosition(int seconds); public abstract void changeTrack(int index, DownloadFile song); + // Really is abstract, just don't want to require RemoteController's support it + public void changeNextTrack(DownloadFile song) { + + }; + public boolean isNextSupported() { + return this.nextSupported; + } public abstract void setVolume(int volume); public abstract void updateVolume(boolean up); public abstract double getVolume(); -- cgit v1.2.3