aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml3
-rw-r--r--libs/cling-core-1.0.5.jarbin0 -> 551922 bytes
-rw-r--r--libs/cling-support-1.0.5.jarbin0 -> 359154 bytes
-rw-r--r--libs/teleal-common-1.0.13.jarbin0 -> 231831 bytes
-rw-r--r--src/github/daneren2005/dsub/domain/DLNADevice.java78
-rw-r--r--src/github/daneren2005/dsub/domain/RemoteControlState.java3
-rw-r--r--src/github/daneren2005/dsub/provider/DLNARouteProvider.java305
-rw-r--r--src/github/daneren2005/dsub/service/ChromeCastController.java20
-rw-r--r--src/github/daneren2005/dsub/service/DLNAController.java366
-rw-r--r--src/github/daneren2005/dsub/service/DownloadService.java12
-rw-r--r--src/github/daneren2005/dsub/util/MediaRouteManager.java13
-rw-r--r--src/github/daneren2005/dsub/util/Util.java16
12 files changed, 789 insertions, 27 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2d69417b..25734167 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
@@ -85,6 +86,8 @@
<service android:name=".service.DownloadService"
android:label="Subsonic Download Service"/>
+ <service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl"/>
+
<service android:name="github.daneren2005.dsub.service.sync.AuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
diff --git a/libs/cling-core-1.0.5.jar b/libs/cling-core-1.0.5.jar
new file mode 100644
index 00000000..8079f329
--- /dev/null
+++ b/libs/cling-core-1.0.5.jar
Binary files differ
diff --git a/libs/cling-support-1.0.5.jar b/libs/cling-support-1.0.5.jar
new file mode 100644
index 00000000..a0ca6363
--- /dev/null
+++ b/libs/cling-support-1.0.5.jar
Binary files differ
diff --git a/libs/teleal-common-1.0.13.jar b/libs/teleal-common-1.0.13.jar
new file mode 100644
index 00000000..2d6403ef
--- /dev/null
+++ b/libs/teleal-common-1.0.13.jar
Binary files differ
diff --git a/src/github/daneren2005/dsub/domain/DLNADevice.java b/src/github/daneren2005/dsub/domain/DLNADevice.java
new file mode 100644
index 00000000..ba4c2777
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/DLNADevice.java
@@ -0,0 +1,78 @@
+/*
+ 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.domain;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.teleal.cling.model.meta.Device;
+
+/**
+ * Created by Scott on 11/1/2014.
+ */
+public class DLNADevice implements Parcelable {
+ public Device renderer;
+ public String id;
+ public String name;
+ public String description;
+ public int volume;
+ public int volumeMax;
+
+ public static final Parcelable.Creator<DLNADevice> CREATOR = new Parcelable.Creator<DLNADevice>() {
+ public DLNADevice createFromParcel(Parcel in) {
+ return new DLNADevice(in);
+ }
+
+ public DLNADevice[] newArray(int size) {
+ return new DLNADevice[size];
+ }
+ };
+
+ private DLNADevice(Parcel in) {
+ id = in.readString();
+ name = in.readString();
+ description = in.readString();
+ volume = in.readInt();
+ volumeMax = in.readInt();
+ }
+
+ public DLNADevice(Device renderer, String id, String name, String description, int volume, int volumeMax) {
+ this.renderer = renderer;
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.volume = volume;
+ this.volumeMax = volumeMax;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(name);
+ dest.writeString(description);
+ dest.writeInt(volume);
+ dest.writeInt(volumeMax);
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/RemoteControlState.java b/src/github/daneren2005/dsub/domain/RemoteControlState.java
index 9bf3bf91..47895984 100644
--- a/src/github/daneren2005/dsub/domain/RemoteControlState.java
+++ b/src/github/daneren2005/dsub/domain/RemoteControlState.java
@@ -23,7 +23,8 @@ public enum RemoteControlState {
LOCAL(0),
JUKEBOX_SERVER(1),
CHROMECAST(2),
- REMOTE_CLIENT(3);
+ REMOTE_CLIENT(3),
+ DLNA(4);
private final int mRemoteControlState;
diff --git a/src/github/daneren2005/dsub/provider/DLNARouteProvider.java b/src/github/daneren2005/dsub/provider/DLNARouteProvider.java
new file mode 100644
index 00000000..44cde0ef
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DLNARouteProvider.java
@@ -0,0 +1,305 @@
+/*
+ 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.os.IBinder;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteDiscoveryRequest;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+import android.util.Log;
+
+import org.teleal.cling.android.AndroidUpnpService;
+import org.teleal.cling.android.AndroidUpnpServiceImpl;
+import org.teleal.cling.model.action.ActionInvocation;
+import org.teleal.cling.model.message.UpnpResponse;
+import org.teleal.cling.model.meta.Device;
+import org.teleal.cling.model.meta.LocalDevice;
+import org.teleal.cling.model.meta.RemoteDevice;
+import org.teleal.cling.model.meta.StateVariable;
+import org.teleal.cling.model.meta.StateVariableAllowedValueRange;
+import org.teleal.cling.model.types.ServiceType;
+import org.teleal.cling.registry.Registry;
+import org.teleal.cling.registry.RegistryListener;
+import org.teleal.cling.support.renderingcontrol.callback.GetVolume;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DLNAController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+/**
+ * Created by Scott on 11/28/13.
+ */
+public class DLNARouteProvider extends MediaRouteProvider {
+ private static final String TAG = DLNARouteProvider.class.getSimpleName();
+ public static final String CATEGORY_DLNA = "github.daneren2005.dsub.DLNA";
+
+ private DownloadService downloadService;
+ private RemoteController controller;
+
+ private HashMap<String, DLNADevice> devices = new HashMap<String, DLNADevice>();
+ private List<String> adding = new ArrayList<String>();
+ private AndroidUpnpService dlnaService;
+ private ServiceConnection dlnaServiceConnection;
+
+ public DLNARouteProvider(Context context) {
+ super(context);
+ this.downloadService = (DownloadService) context;
+ dlnaServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ dlnaService = (AndroidUpnpService) service;
+ dlnaService.getRegistry().addListener(new RegistryListener() {
+ @Override
+ public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
+
+ }
+
+ @Override
+ public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) {
+
+ }
+
+ @Override
+ public void remoteDeviceAdded(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceUpdated(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceRemoved(Registry registry, RemoteDevice remoteDevice) {
+ deviceRemoved(remoteDevice);
+ }
+
+ @Override
+ public void localDeviceAdded(Registry registry, LocalDevice localDevice) {
+ deviceAdded(localDevice);
+ }
+
+ @Override
+ public void localDeviceRemoved(Registry registry, LocalDevice localDevice) {
+ deviceRemoved(localDevice);
+ }
+
+ @Override
+ public void beforeShutdown(Registry registry) {
+
+ }
+
+ @Override
+ public void afterShutdown() {
+
+ }
+ });
+
+ for (Device<?, ?, ?> device : dlnaService.getControlPoint().getRegistry().getDevices()) {
+ deviceAdded(device);
+ }
+ dlnaService.getControlPoint().search();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ dlnaService = null;
+ }
+ };
+ context.bindService(new Intent(context, AndroidUpnpServiceImpl.class), dlnaServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void broadcastDescriptors() {
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(CATEGORY_DLNA);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+
+ // Create route descriptor
+ for(Map.Entry<String, DLNADevice> deviceEntry: devices.entrySet()) {
+ DLNADevice device = deviceEntry.getValue();
+
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder(device.id, device.name);
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription(device.description)
+ .setVolume(controller == null ? 5 : (int) (controller.getVolume() * 10))
+ .setVolumeMax(device.volumeMax)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+ providerBuilder.addRoute(routeBuilder.build());
+ }
+
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ if (request != null && request.isActiveScan()) {
+
+ }
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ DLNADevice device = devices.get(routeId);
+ if(device == null) {
+ Log.w(TAG, "No device exists for " + routeId);
+ return null;
+ }
+
+ return new DLNARouteController(device);
+ }
+
+ private void deviceAdded(final Device device) {
+ final org.teleal.cling.model.meta.Service renderingControl = device.findService(new ServiceType("schemas-upnp-org", "RenderingControl"));
+ if(renderingControl == null) {
+ return;
+ }
+
+ final String id = device.getIdentity().getUdn().toString();
+ // In the process of looking up it's details already
+ if(adding.contains(id)) {
+ return;
+ }
+ adding.add(id);
+
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ dlnaService.getControlPoint().execute(new GetVolume(renderingControl) {
+ @Override
+ public void received(ActionInvocation actionInvocation, int currentVolume) {
+ int maxVolume = 100;
+ StateVariable volume = renderingControl.getStateVariable("Volume");
+ if(volume != null) {
+ StateVariableAllowedValueRange volumeRange = volume.getTypeDetails().getAllowedValueRange();
+ maxVolume = (int) volumeRange.getMaximum();
+ }
+
+ // Create a new DLNADevice to represent this item
+ String id = device.getIdentity().getUdn().toString();
+ String name = device.getDetails().getFriendlyName();
+ String displayName = device.getDisplayString();
+
+ DLNADevice newDevice = new DLNADevice(device, id, name, displayName, currentVolume, maxVolume);
+ devices.put(id, newDevice);
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ broadcastDescriptors();
+ }
+ });
+ adding.remove(id);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get default volume for DLNA route");
+ Log.w(TAG, "Reason: " + s);
+ adding.remove(id);
+ }
+ });
+ }
+ }
+ private void deviceRemoved(Device device) {
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ String id = device.getIdentity().getUdn().toString();
+ devices.remove(id);
+
+ // Make sure we do this on the main thread
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ broadcastDescriptors();
+ }
+ });
+ }
+ }
+
+ private class DLNARouteController extends RouteController {
+ private DLNADevice device;
+
+ public DLNARouteController(DLNADevice device) {
+ this.device = device;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(CATEGORY_DLNA)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onSelect() {
+ controller = new DLNAController(downloadService, dlnaService.getControlPoint(), device);
+ downloadService.setRemoteEnabled(RemoteControlState.DLNA, controller);
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if(controller != null) {
+ controller.updateVolume(delta > 0);
+ }
+ broadcastDescriptors();
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if(controller != null) {
+ controller.setVolume(volume);
+ }
+ broadcastDescriptors();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/ChromeCastController.java b/src/github/daneren2005/dsub/service/ChromeCastController.java
index 0c8f38a6..6f35ac1d 100644
--- a/src/github/daneren2005/dsub/service/ChromeCastController.java
+++ b/src/github/daneren2005/dsub/service/ChromeCastController.java
@@ -283,7 +283,7 @@ public class ChromeCastController extends RemoteController {
url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate());
}
- url = fixURLs(url);
+ url = Util.replaceInternalUrl(downloadService, url);
}
// Setup song/video information
@@ -300,7 +300,7 @@ public class ChromeCastController extends RemoteController {
String coverArt = "";
if(proxy == null) {
coverArt = musicService.getCoverArtUrl(downloadService, song);
- coverArt = fixURLs(coverArt);
+ coverArt = Util.replaceInternalUrl(downloadService, coverArt);
meta.addImage(new WebImage(Uri.parse(coverArt)));
} else {
File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song);
@@ -354,22 +354,6 @@ public class ChromeCastController extends RemoteController {
}
}
- private String fixURLs(String url) {
- // Only change to internal when using https
- if(url.indexOf("https") != -1) {
- SharedPreferences prefs = Util.getPreferences(downloadService);
- int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
- String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
- if(internalUrl != null && !"".equals(internalUrl)) {
- String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
- url = url.replace(internalUrl, externalUrl);
- }
- }
-
- // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC
- return url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID);
- }
-
private void failedLoad() {
Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
downloadService.setPlayerState(PlayerState.STOPPED);
diff --git a/src/github/daneren2005/dsub/service/DLNAController.java b/src/github/daneren2005/dsub/service/DLNAController.java
new file mode 100644
index 00000000..2448b088
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/DLNAController.java
@@ -0,0 +1,366 @@
+/*
+ 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.os.Looper;
+import android.util.Log;
+
+import org.teleal.cling.controlpoint.ControlPoint;
+import org.teleal.cling.controlpoint.SubscriptionCallback;
+import org.teleal.cling.model.action.ActionInvocation;
+import org.teleal.cling.model.gena.CancelReason;
+import org.teleal.cling.model.gena.GENASubscription;
+import org.teleal.cling.model.message.UpnpResponse;
+import org.teleal.cling.model.meta.Device;
+import org.teleal.cling.model.meta.StateVariable;
+import org.teleal.cling.model.state.StateVariableValue;
+import org.teleal.cling.model.types.ServiceType;
+import org.teleal.cling.support.avtransport.callback.Pause;
+import org.teleal.cling.support.avtransport.callback.Play;
+import org.teleal.cling.support.avtransport.callback.Seek;
+import org.teleal.cling.support.avtransport.callback.SetAVTransportURI;
+import org.teleal.cling.support.avtransport.callback.Stop;
+import org.teleal.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
+import org.teleal.cling.support.avtransport.lastchange.AVTransportVariable;
+import org.teleal.cling.support.contentdirectory.DIDLParser;
+import org.teleal.cling.support.lastchange.LastChange;
+import org.teleal.cling.support.model.DIDLContent;
+import org.teleal.cling.support.model.SeekMode;
+import org.teleal.cling.support.model.item.Item;
+import org.teleal.cling.support.model.item.MusicTrack;
+import org.teleal.cling.support.model.item.VideoItem;
+import org.teleal.cling.support.renderingcontrol.callback.SetVolume;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.TimeZone;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.DLNADevice;
+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.serverproxy.FileProxy;
+
+public class DLNAController extends RemoteController {
+ private static final String TAG = DLNAController.class.getSimpleName();
+
+ DLNADevice device;
+ ControlPoint controlPoint;
+ SubscriptionCallback callback;
+
+ private FileProxy proxy;
+ String rootLocation = "";
+ boolean error = false;
+
+ public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) {
+ this.downloadService = downloadService;
+ this.controlPoint = controlPoint;
+ this.device = device;
+
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ }
+
+ @Override
+ public void create(final boolean playing, final int seconds) {
+ downloadService.setPlayerState(PlayerState.PREPARING);
+
+ Device renderer = device.renderer;
+
+ callback = new SubscriptionCallback(renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport")), 600) {
+ @Override
+ protected void failed(GENASubscription genaSubscription, UpnpResponse upnpResponse, Exception e, String msg) {
+ Log.w(TAG, "Register subscription callback failed: " + msg, e);
+ }
+
+ @Override
+ protected void established(GENASubscription genaSubscription) {
+ startSong(downloadService.getCurrentPlaying(), playing, seconds);
+ }
+
+ @Override
+ protected void ended(GENASubscription genaSubscription, CancelReason cancelReason, UpnpResponse upnpResponse) {
+
+ }
+
+ @Override
+ protected void eventReceived(GENASubscription genaSubscription) {
+ Map<String, StateVariableValue> m = genaSubscription.getCurrentValues();
+ try {
+ LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString());
+ if (playing || lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) {
+ return;
+ }
+
+ switch (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class).getValue()) {
+ case PLAYING:
+ downloadService.setPlayerState(PlayerState.STARTED);
+ break;
+ case PAUSED_PLAYBACK:
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ break;
+ case STOPPED:
+ boolean failed = false;
+ for(StateVariableValue val: m.values()) {
+ if(val.toString().indexOf("TransportStatus val=\"ERROR_OCCURRED\"") != -1) {
+ Log.w(TAG, "Failed to load with event: val.toString()");
+ failed = true;
+ }
+ }
+
+ if(failed) {
+ failedLoad();
+ } else {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ }
+ break;
+ case TRANSITIONING:
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ break;
+ case NO_MEDIA_PRESENT:
+ downloadService.setPlayerState(PlayerState.IDLE);
+ break;
+ default:
+ }
+ }
+ catch (Exception e) {
+ Log.w(TAG, "Failed to parse UPNP event", e);
+ failedLoad();
+ }
+ }
+
+ @Override
+ protected void eventsMissed(GENASubscription genaSubscription, int i) {
+
+ }
+ };
+ controlPoint.execute(callback);
+ }
+
+ @Override
+ public void start() {
+ if(error) {
+ Log.w(TAG, "Attempting to restart song");
+ startSong(downloadService.getCurrentPlaying(), true, 0);
+ return;
+ }
+
+ controlPoint.execute(new Play(device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"))) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to start playing: " + msg);
+ failedLoad();
+ }
+ });
+ }
+
+ @Override
+ public void stop() {
+ controlPoint.execute(new Pause(device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"))) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to pause playing: " + msg);
+ }
+ });
+ }
+
+ @Override
+ public void shutdown() {
+ controlPoint.execute(new Stop(device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"))) {
+ @Override
+ public void failure(ActionInvocation invocation, org.teleal.cling.model.message.UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Stop failed: " + defaultMessage);
+ }
+ });
+
+ if(callback != null) {
+ callback.end();
+ callback = null;
+ }
+ }
+
+ @Override
+ public void updatePlaylist() {
+ if(downloadService.getCurrentPlaying() == null) {
+ startSong(null, false, 0);
+ }
+ }
+
+ @Override
+ public void changePosition(int seconds) {
+ SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ controlPoint.execute(new Seek(device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport")), SeekMode.REL_TIME, df.format(new Date(seconds * 1000))) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Seek failed: " + defaultMessage);
+ }
+ });
+ }
+
+ @Override
+ public void changeTrack(int index, DownloadFile song) {
+ startSong(song, true, 0);
+ }
+
+ @Override
+ public void setVolume(int volume) {
+ device.volume = volume;
+ controlPoint.execute(new SetVolume(device.renderer.findService(new ServiceType("schemas-upnp-org", "RenderingControl")), volume) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Set volume failed: " + defaultMessage);
+ }
+ });
+ }
+
+ @Override
+ public void updateVolume(boolean up) {
+ setVolume(device.volume + (up ? 1 : -1));
+ }
+
+ @Override
+ public double getVolume() {
+ return device.volume;
+ }
+
+ @Override
+ public int getRemotePosition() {
+ return 0;
+ }
+
+ private void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
+ if(currentPlaying == null) {
+ downloadService.setPlayerState(PlayerState.IDLE);
+ return;
+ }
+ error = false;
+
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ MusicDirectory.Entry song = currentPlaying.getSong();
+
+ try {
+ // Get url for entry
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ String url;
+ if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
+ if(proxy == null) {
+ proxy = new FileProxy(downloadService);
+ proxy.start();
+ }
+
+ url = proxy.getPublicAddress(song.getId());
+ } 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());
+ }
+
+ url = Util.replaceInternalUrl(downloadService, url);
+ }
+
+ // Create metadata for entry
+ Item track;
+ if(song.isVideo()) {
+ track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist());
+ } else {
+ MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist());
+ musicTrack.setOriginalTrackNumber(song.getTrack());
+ 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);
+ }
+
+ controlPoint.execute(new SetAVTransportURI(device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport")), url, metadata) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ if (autoStart) {
+ controlPoint.execute(new Play(device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"))) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to start playing: " + msg);
+ failedLoad();
+ }
+ });
+ } else {
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+ }
+
+ @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 failedLoad() {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ error = true;
+
+ if(Looper.myLooper() != Looper.getMainLooper()) {
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ });
+ } else {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java
index c4dd3546..6482cc14 100644
--- a/src/github/daneren2005/dsub/service/DownloadService.java
+++ b/src/github/daneren2005/dsub/service/DownloadService.java
@@ -53,7 +53,6 @@ import github.daneren2005.dsub.view.UpdateView;
import github.daneren2005.serverproxy.BufferProxy;
import java.io.File;
-import java.io.IOError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@@ -70,7 +69,6 @@ import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.audiofx.AudioEffect;
-import android.media.audiofx.Equalizer;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
@@ -300,6 +298,10 @@ public class DownloadService extends Service {
public IBinder onBind(Intent intent) {
return binder;
}
+
+ public void post(Runnable r) {
+ handler.post(r);
+ }
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
download(songs, save, autoplay, playNext, shuffle, 0, 0);
@@ -573,9 +575,9 @@ public class DownloadService extends Service {
public void setOnline(final boolean online) {
if(online) {
- mediaRouter.addOfflineProviders();
+ mediaRouter.addOnlineProviders();
} else {
- mediaRouter.removeOfflineProviders();
+ mediaRouter.removeOnlineProviders();
}
if(shufflePlay) {
setShufflePlayEnabled(false);
@@ -1343,7 +1345,7 @@ public class DownloadService extends Service {
case JUKEBOX_SERVER:
remoteController = new JukeboxController(this, handler);
break;
- case CHROMECAST:
+ case CHROMECAST: case DLNA:
if(ref == null) {
remoteState = RemoteControlState.LOCAL;
break;
diff --git a/src/github/daneren2005/dsub/util/MediaRouteManager.java b/src/github/daneren2005/dsub/util/MediaRouteManager.java
index 347b0376..11e0d387 100644
--- a/src/github/daneren2005/dsub/util/MediaRouteManager.java
+++ b/src/github/daneren2005/dsub/util/MediaRouteManager.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.List;
import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.provider.DLNARouteProvider;
import github.daneren2005.dsub.provider.JukeboxRouteProvider;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.RemoteController;
@@ -140,13 +141,18 @@ public class MediaRouteManager extends MediaRouter.Callback {
}
}
- public void addOfflineProviders() {
+ public void addOnlineProviders() {
JukeboxRouteProvider jukeboxProvider = new JukeboxRouteProvider(downloadService);
router.addProvider(jukeboxProvider);
providers.add(jukeboxProvider);
offlineProviders.add(jukeboxProvider);
+
+ DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService);
+ router.addProvider(dlnaProvider);
+ providers.add(dlnaProvider);
+ offlineProviders.add(dlnaProvider);
}
- public void removeOfflineProviders() {
+ public void removeOnlineProviders() {
for(MediaRouteProvider provider: offlineProviders) {
router.removeProvider(provider);
}
@@ -154,7 +160,7 @@ public class MediaRouteManager extends MediaRouter.Callback {
private void addProviders() {
if(!Util.isOffline(downloadService)) {
- addOfflineProviders();
+ addOnlineProviders();
}
}
public void buildSelector() {
@@ -165,6 +171,7 @@ public class MediaRouteManager extends MediaRouter.Callback {
if(castAvailable) {
builder.addControlCategory(CastCompat.getCastControlCategory());
}
+ builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA);
selector = builder.build();
}
}
diff --git a/src/github/daneren2005/dsub/util/Util.java b/src/github/daneren2005/dsub/util/Util.java
index e9724ded..205690f8 100644
--- a/src/github/daneren2005/dsub/util/Util.java
+++ b/src/github/daneren2005/dsub/util/Util.java
@@ -357,6 +357,22 @@ public final class Util {
return builder.toString();
}
+ public static String replaceInternalUrl(Context context, String url) {
+ // Only change to internal when using https
+ if(url.indexOf("https") != -1) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
+ if(internalUrl != null && !"".equals(internalUrl)) {
+ String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ url = url.replace(internalUrl, externalUrl);
+ }
+ }
+
+ // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC
+ return url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID);
+ }
+
public static boolean isTagBrowsing(Context context) {
return isTagBrowsing(context, Util.getActiveServer(context));
}