Added MediaRouter support.
This commit is contained in:
parent
048fad32f6
commit
5edbd2886f
10 changed files with 719 additions and 30 deletions
|
@ -36,6 +36,16 @@
|
||||||
|
|
||||||
<service android:name="com.github.nutomic.controldlna.upnp.PlayService" />
|
<service android:name="com.github.nutomic.controldlna.upnp.PlayService" />
|
||||||
|
|
||||||
|
<service android:name="com.github.nutomic.controldlna.mediarouter.RemotePlayService" />
|
||||||
|
|
||||||
|
<service android:name=".mediarouter.ProviderService"
|
||||||
|
android:label="sample_media_route_provider_service"
|
||||||
|
android:process=":mrp">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.MediaRouteProviderService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -13,3 +13,4 @@
|
||||||
# Project target.
|
# Project target.
|
||||||
target=android-18
|
target=android-18
|
||||||
android.library.reference.1=../android-support-v7-appcompat
|
android.library.reference.1=../android-support-v7-appcompat
|
||||||
|
android.library.reference.2=../android-support-v7-mediarouter
|
||||||
|
|
|
@ -65,6 +65,7 @@ import android.widget.SeekBar.OnSeekBarChangeListener;
|
||||||
|
|
||||||
import com.github.nutomic.controldlna.R;
|
import com.github.nutomic.controldlna.R;
|
||||||
import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener;
|
import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener;
|
||||||
|
import com.github.nutomic.controldlna.upnp.UpnpController;
|
||||||
import com.github.nutomic.controldlna.upnp.UpnpPlayer;
|
import com.github.nutomic.controldlna.upnp.UpnpPlayer;
|
||||||
import com.github.nutomic.controldlna.utility.DeviceArrayAdapter;
|
import com.github.nutomic.controldlna.utility.DeviceArrayAdapter;
|
||||||
import com.github.nutomic.controldlna.utility.FileArrayAdapter;
|
import com.github.nutomic.controldlna.utility.FileArrayAdapter;
|
||||||
|
@ -146,7 +147,7 @@ public class RendererFragment extends Fragment implements
|
||||||
* playback is active.
|
* playback is active.
|
||||||
*/
|
*/
|
||||||
private void pollTimePosition() {
|
private void pollTimePosition() {
|
||||||
Service<?, ?> service = getPlayer()
|
Service<?, ?> service = UpnpController
|
||||||
.getService(mCurrentRenderer, "AVTransport");
|
.getService(mCurrentRenderer, "AVTransport");
|
||||||
getPlayer().execute(
|
getPlayer().execute(
|
||||||
new GetPositionInfo(service) {
|
new GetPositionInfo(service) {
|
||||||
|
|
358
src/com/github/nutomic/controldlna/mediarouter/Provider.java
Normal file
358
src/com/github/nutomic/controldlna/mediarouter/Provider.java
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2013, Felix Ableitner
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the <organization> nor the
|
||||||
|
names of its contributors may be used to endorse or promote products
|
||||||
|
derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.nutomic.controldlna.mediarouter;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.IntentFilter.MalformedMimeTypeException;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
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.Builder;
|
||||||
|
import android.support.v7.media.MediaRouter;
|
||||||
|
import android.support.v7.media.MediaRouter.ControlRequestCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows playing to a DLNA renderer from a remote app.
|
||||||
|
*
|
||||||
|
* @author Felix Ableitner
|
||||||
|
*/
|
||||||
|
final class Provider extends MediaRouteProvider {
|
||||||
|
|
||||||
|
// Device has been added.
|
||||||
|
// param: Device device
|
||||||
|
public static final int MSG_RENDERER_ADDED = 1;
|
||||||
|
// Device has been removed.
|
||||||
|
// param: int id
|
||||||
|
public static final int MSG_RENDERER_REMOVED = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows passing and storing basic information about a device.
|
||||||
|
*/
|
||||||
|
static public class Device implements Parcelable {
|
||||||
|
|
||||||
|
public String id;
|
||||||
|
public String name;
|
||||||
|
public String description;
|
||||||
|
public int volume;
|
||||||
|
public int volumeMax;
|
||||||
|
|
||||||
|
public static final Parcelable.Creator<Device> CREATOR
|
||||||
|
= new Parcelable.Creator<Device>() {
|
||||||
|
public Device createFromParcel(Parcel in) {
|
||||||
|
return new Device(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Device[] newArray(int size) {
|
||||||
|
return new Device[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private Device(Parcel in) {
|
||||||
|
id = in.readString();
|
||||||
|
name = in.readString();
|
||||||
|
description = in.readString();
|
||||||
|
volume = in.readInt();
|
||||||
|
volumeMax = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Device(String id, String name, String description, int volume, int volumeMax) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashMap<String, Device> mDevices = new HashMap<String, Device>();
|
||||||
|
|
||||||
|
private Messenger mRemotePlayService;
|
||||||
|
|
||||||
|
private ServiceConnection mConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||||
|
mRemotePlayService = new Messenger(service);
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_OPEN, 0, 0);
|
||||||
|
msg.replyTo = mListener;
|
||||||
|
try {
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
mRemotePlayService = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final ArrayList<IntentFilter> CONTROL_FILTERS;
|
||||||
|
// Static constructor for CONTROL_FILTERS.
|
||||||
|
static {
|
||||||
|
|
||||||
|
IntentFilter f = new IntentFilter();
|
||||||
|
f.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
||||||
|
f.addAction(MediaControlIntent.ACTION_PLAY);
|
||||||
|
f.addAction(MediaControlIntent.ACTION_PAUSE);
|
||||||
|
f.addAction(MediaControlIntent.ACTION_SEEK);
|
||||||
|
f.addAction(MediaControlIntent.ACTION_STOP);
|
||||||
|
f.addDataScheme("http");
|
||||||
|
f.addDataScheme("https");
|
||||||
|
try {
|
||||||
|
f.addDataType("video/*");
|
||||||
|
f.addDataType("audio/*");
|
||||||
|
} catch (MalformedMimeTypeException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTROL_FILTERS = new ArrayList<IntentFilter>();
|
||||||
|
CONTROL_FILTERS.add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for messages about devices.
|
||||||
|
*/
|
||||||
|
static private class DeviceListener extends Handler {
|
||||||
|
|
||||||
|
private final WeakReference<Provider> mService;
|
||||||
|
|
||||||
|
DeviceListener(Provider provider) {
|
||||||
|
mService = new WeakReference<Provider>(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
if (mService.get() != null) {
|
||||||
|
mService.get().handleMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Messenger mListener = new Messenger(new DeviceListener(this));
|
||||||
|
|
||||||
|
public Provider(Context context) {
|
||||||
|
super(context);
|
||||||
|
context.bindService(
|
||||||
|
new Intent(context, RemotePlayService.class),
|
||||||
|
mConnection,
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
getContext().unbindService(mConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
|
||||||
|
if (request == null) return;
|
||||||
|
Message msg;
|
||||||
|
if (request.isActiveScan()) {
|
||||||
|
msg = Message.obtain(null, RemotePlayService.MSG_OPEN, 0, 0);
|
||||||
|
msg.replyTo = mListener;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = Message.obtain(null, RemotePlayService.MSG_CLOSE, 0, 0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (mRemotePlayService != null) {
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RouteController onCreateRouteController(String routeId) {
|
||||||
|
return new RouteController(routeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRoutes() {
|
||||||
|
Builder builder = new Builder();
|
||||||
|
for (Entry<String, Device> d : mDevices.entrySet()) {
|
||||||
|
MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
|
||||||
|
d.getValue().id,
|
||||||
|
d.getValue().name)
|
||||||
|
.setDescription("DLNA Playback")
|
||||||
|
.addControlFilters(CONTROL_FILTERS)
|
||||||
|
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
|
||||||
|
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
|
||||||
|
.setVolumeMax(d.getValue().volumeMax)
|
||||||
|
.setVolume(d.getValue().volume)
|
||||||
|
.build();
|
||||||
|
builder.addRoute(routeDescriptor);
|
||||||
|
}
|
||||||
|
setDescriptor(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives and forwards device selections, volume change
|
||||||
|
* requests and control requests.
|
||||||
|
*/
|
||||||
|
private final class RouteController extends MediaRouteProvider.RouteController {
|
||||||
|
private final String mRouteId;
|
||||||
|
|
||||||
|
public RouteController(String routeId) {
|
||||||
|
mRouteId = routeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSelect() {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_SELECT, 0, 0);
|
||||||
|
msg.getData().putString("id", mRouteId);
|
||||||
|
try {
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUnselect() {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_UNSELECT, 0, 0);
|
||||||
|
msg.getData().putString("id", mRouteId);
|
||||||
|
try {
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetVolume(int volume) {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_SET_VOPLUME, 0, 0);
|
||||||
|
msg.getData().putInt("volume", volume);
|
||||||
|
mDevices.get(mRouteId).volume = volume;
|
||||||
|
try {
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
updateRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdateVolume(int delta) {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_CHANGE_VOLUME, 0, 0);
|
||||||
|
msg.getData().putInt("delta", delta);
|
||||||
|
mDevices.get(mRouteId).volume += delta;
|
||||||
|
try {
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
updateRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
|
||||||
|
try {
|
||||||
|
if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_PLAY, 0, 0);
|
||||||
|
msg.getData().putString("uri", intent.getDataString());
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (intent.getAction().equals(MediaControlIntent.ACTION_PAUSE)) {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_PAUSE, 0, 0);
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_STOP, 0, 0);
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) {
|
||||||
|
Message msg = Message.obtain(null, RemotePlayService.MSG_PLAY, 0, 0);
|
||||||
|
msg.getData().putLong("milliseconds",
|
||||||
|
intent.getIntExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0));
|
||||||
|
mRemotePlayService.send(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
Bundle data = msg.getData();
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_RENDERER_ADDED:
|
||||||
|
msg.getData().setClassLoader(Device.class.getClassLoader());
|
||||||
|
Device device = (Device) data.getParcelable("device");
|
||||||
|
mDevices.put(device.id, device);
|
||||||
|
updateRoutes();
|
||||||
|
break;
|
||||||
|
case MSG_RENDERER_REMOVED:
|
||||||
|
mDevices.remove(data.getString("id"));
|
||||||
|
updateRoutes();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2013, Felix Ableitner
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the <organization> nor the
|
||||||
|
names of its contributors may be used to endorse or promote products
|
||||||
|
derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.nutomic.controldlna.mediarouter;
|
||||||
|
|
||||||
|
import android.support.v7.media.MediaRouteProvider;
|
||||||
|
import android.support.v7.media.MediaRouteProviderService;
|
||||||
|
|
||||||
|
public class ProviderService extends MediaRouteProviderService {
|
||||||
|
|
||||||
|
private Provider mProvider;
|
||||||
|
@Override
|
||||||
|
public MediaRouteProvider onCreateMediaRouteProvider() {
|
||||||
|
if (mProvider == null) {
|
||||||
|
mProvider = new Provider(this);
|
||||||
|
}
|
||||||
|
return mProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
mProvider.close();
|
||||||
|
mProvider = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2013, Felix Ableitner
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the <organization> nor the
|
||||||
|
names of its contributors may be used to endorse or promote products
|
||||||
|
derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.nutomic.controldlna.mediarouter;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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.RemoteDevice;
|
||||||
|
import org.teleal.cling.model.meta.StateVariableAllowedValueRange;
|
||||||
|
import org.teleal.cling.support.model.Res;
|
||||||
|
import org.teleal.cling.support.model.item.Item;
|
||||||
|
import org.teleal.cling.support.renderingcontrol.callback.GetVolume;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.github.nutomic.controldlna.upnp.DeviceListener.DeviceListenerCallback;
|
||||||
|
import com.github.nutomic.controldlna.upnp.UpnpController;
|
||||||
|
import com.github.nutomic.controldlna.upnp.UpnpPlayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows UPNP playback from within different apps by providing a proxy interface.
|
||||||
|
*
|
||||||
|
* @author Felix Ableitner
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class RemotePlayService extends Service implements DeviceListenerCallback {
|
||||||
|
|
||||||
|
private static final String TAG = "RemotePlayService";
|
||||||
|
|
||||||
|
// Start device discovery.
|
||||||
|
public static final int MSG_OPEN = 1;
|
||||||
|
// Stop device discovery.
|
||||||
|
public static final int MSG_CLOSE = 2;
|
||||||
|
// Select renderer.
|
||||||
|
// param: string device_id
|
||||||
|
public static final int MSG_SELECT = 3;
|
||||||
|
// Unselect renderer.
|
||||||
|
// param: int device_id
|
||||||
|
public static final int MSG_UNSELECT = 4;
|
||||||
|
// Set absolute volume.
|
||||||
|
// param: int volume
|
||||||
|
public static final int MSG_SET_VOPLUME = 5;
|
||||||
|
// Change volume relative to current volume.
|
||||||
|
// param: int delta
|
||||||
|
public static final int MSG_CHANGE_VOLUME = 6;
|
||||||
|
// Play from uri.
|
||||||
|
// param: String uri
|
||||||
|
public static final int MSG_PLAY = 7;
|
||||||
|
// Pause playback.
|
||||||
|
public static final int MSG_PAUSE = 8;
|
||||||
|
// Stop playback.
|
||||||
|
public static final int MSG_STOP = 9;
|
||||||
|
// Seek to absolute time in ms.
|
||||||
|
// param: long milliseconds
|
||||||
|
public static final int MSG_SEEK = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming messages from clients.
|
||||||
|
*/
|
||||||
|
private static class IncomingHandler extends Handler {
|
||||||
|
|
||||||
|
private final WeakReference<RemotePlayService> mService;
|
||||||
|
|
||||||
|
IncomingHandler(RemotePlayService service) {
|
||||||
|
mService = new WeakReference<RemotePlayService>(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
if (mService.get() != null) {
|
||||||
|
mService.get().handleMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Messenger mMessenger = new Messenger(new IncomingHandler(this));
|
||||||
|
|
||||||
|
private final UpnpPlayer mPlayer = new UpnpPlayer();
|
||||||
|
|
||||||
|
private Messenger mListener;
|
||||||
|
|
||||||
|
private HashMap<String, Device<?, ?, ?>> mDevices = new HashMap<String, Device<?, ?, ?>>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent itnent) {
|
||||||
|
return mMessenger.getBinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
mPlayer.open(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (mPlayer.getPlayService().getRenderer() != null) {
|
||||||
|
mPlayer.getPlayService().stop();
|
||||||
|
}
|
||||||
|
mPlayer.close(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deviceAdded(final Device<?, ?, ?> device) {
|
||||||
|
if (device.getType().getType().equals("MediaRenderer") &&
|
||||||
|
device instanceof RemoteDevice) {
|
||||||
|
mDevices.put(device.getIdentity().getUdn().toString(), device);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mPlayer.execute(
|
||||||
|
new GetVolume(UpnpController.getService(device, "RenderingControl")) {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
@Override
|
||||||
|
public void failure(ActionInvocation invocation,
|
||||||
|
UpnpResponse operation, String defaultMessage) {
|
||||||
|
Log.w(TAG, "Failed to get current Volume: " + defaultMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
@Override
|
||||||
|
public void received(ActionInvocation invocation, int currentVolume) {
|
||||||
|
int maxVolume = 100;
|
||||||
|
if (UpnpPlayer.getService(device, "RenderingControl").getStateVariable("Volume") != null) {
|
||||||
|
StateVariableAllowedValueRange volumeRange =
|
||||||
|
UpnpPlayer.getService(device, "RenderingControl").getStateVariable("Volume")
|
||||||
|
.getTypeDetails().getAllowedValueRange();
|
||||||
|
maxVolume = (int) volumeRange.getMaximum();
|
||||||
|
}
|
||||||
|
|
||||||
|
Message msg = Message.obtain(null, Provider.MSG_RENDERER_ADDED, 0, 0);
|
||||||
|
msg.getData().putParcelable("device", new Provider.Device(
|
||||||
|
device.getIdentity().getUdn().toString(),
|
||||||
|
device.getDisplayString(),
|
||||||
|
device.getDetails().getManufacturerDetails().getManufacturer(),
|
||||||
|
currentVolume,
|
||||||
|
maxVolume));
|
||||||
|
try {
|
||||||
|
mListener.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deviceRemoved(Device<?, ?, ?> device) {
|
||||||
|
if (device.getType().getType().equals("MediaRenderer") &&
|
||||||
|
device instanceof RemoteDevice) {
|
||||||
|
Message msg = Message.obtain(null, Provider.MSG_RENDERER_REMOVED, 0, 0);
|
||||||
|
|
||||||
|
String udn = device.getIdentity().getUdn().toString();
|
||||||
|
msg.getData().putString("id", udn);
|
||||||
|
mDevices.remove(udn);
|
||||||
|
try {
|
||||||
|
mListener.send(msg);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
Bundle data = msg.getData();
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_OPEN:
|
||||||
|
mPlayer.getDeviceListener().addCallback(this);
|
||||||
|
mListener = msg.replyTo;
|
||||||
|
break;
|
||||||
|
case MSG_CLOSE:
|
||||||
|
break;
|
||||||
|
case MSG_SELECT:
|
||||||
|
mPlayer.selectRenderer(mDevices.get(data.getString("id")));
|
||||||
|
break;
|
||||||
|
case MSG_UNSELECT:
|
||||||
|
mPlayer.getPlayService().stop();
|
||||||
|
break;
|
||||||
|
case MSG_SET_VOPLUME:
|
||||||
|
mPlayer.setVolume(data.getInt("volume"));
|
||||||
|
break;
|
||||||
|
case MSG_CHANGE_VOLUME:
|
||||||
|
mPlayer.changeVolume(data.getInt("delta"));
|
||||||
|
break;
|
||||||
|
case MSG_PLAY:
|
||||||
|
mPlayer.getPlayService().setShowNotification(false);
|
||||||
|
Item item = new Item();
|
||||||
|
item.addResource(new Res());
|
||||||
|
item.getFirstResource().setValue(data.getString("uri"));
|
||||||
|
List<Item> playlist = new ArrayList<Item>();
|
||||||
|
playlist.add(item);
|
||||||
|
mPlayer.getPlayService().setPlaylist(playlist, 0);
|
||||||
|
break;
|
||||||
|
case MSG_PAUSE:
|
||||||
|
mPlayer.getPlayService().pause();
|
||||||
|
break;
|
||||||
|
case MSG_STOP:
|
||||||
|
mPlayer.getPlayService().stop();
|
||||||
|
break;
|
||||||
|
case MSG_SEEK:
|
||||||
|
mPlayer.seek((int) data.getLong("milliseconds") / 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -64,9 +64,9 @@ public class DeviceListener implements RegistryListener {
|
||||||
|
|
||||||
public void addCallback(DeviceListenerCallback callback) {
|
public void addCallback(DeviceListenerCallback callback) {
|
||||||
mListeners.add(callback);
|
mListeners.add(callback);
|
||||||
for (Device<?, ?, ?> d : mDevices)
|
for (Device<?, ?, ?> d : mDevices) {
|
||||||
callback.deviceAdded(d);
|
callback.deviceAdded(d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeCallback(DeviceListenerCallback callback) {
|
public void removeCallback(DeviceListenerCallback callback) {
|
||||||
|
@ -75,15 +75,17 @@ public class DeviceListener implements RegistryListener {
|
||||||
|
|
||||||
private void deviceAdded(Device<?, ?, ?> device) {
|
private void deviceAdded(Device<?, ?, ?> device) {
|
||||||
mDevices.add(device);
|
mDevices.add(device);
|
||||||
for (DeviceListenerCallback l : mListeners)
|
for (DeviceListenerCallback l : mListeners) {
|
||||||
l.deviceAdded(device);
|
l.deviceAdded(device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void deviceRemoved(Device<?, ?, ?> device) {
|
private void deviceRemoved(Device<?, ?, ?> device) {
|
||||||
mDevices.remove(device);
|
mDevices.remove(device);
|
||||||
for (DeviceListenerCallback l : mListeners)
|
for (DeviceListenerCallback l : mListeners) {
|
||||||
l.deviceRemoved(device);
|
l.deviceRemoved(device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterShutdown() {
|
public void afterShutdown() {
|
||||||
|
@ -127,7 +129,6 @@ public class DeviceListener implements RegistryListener {
|
||||||
public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {
|
public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {
|
||||||
deviceRemoved(device);
|
deviceRemoved(device);
|
||||||
deviceAdded(device);
|
deviceAdded(device);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,8 @@ public class PlayService extends Service {
|
||||||
|
|
||||||
private static final int NOTIFICATION_ID = 1;
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
private boolean mShowNotification = true;
|
||||||
|
|
||||||
private final PlayServiceBinder mBinder = new PlayServiceBinder(this);
|
private final PlayServiceBinder mBinder = new PlayServiceBinder(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,9 +207,14 @@ public class PlayService extends Service {
|
||||||
Log.w(TAG, "Metadata serialization failed", e);
|
Log.w(TAG, "Metadata serialization failed", e);
|
||||||
metadata = "NO METADATA";
|
metadata = "NO METADATA";
|
||||||
}
|
}
|
||||||
|
setTransportUri(metadata,
|
||||||
|
mPlaylist.get(track).getFirstResource().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTransportUri(String metadata, final String uri) {
|
||||||
mUpnpService.getControlPoint().execute(new SetAVTransportURI(
|
mUpnpService.getControlPoint().execute(new SetAVTransportURI(
|
||||||
mAvTransportService,
|
mAvTransportService,
|
||||||
mPlaylist.get(track).getFirstResource().getValue(), metadata) {
|
uri, metadata) {
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
@Override
|
@Override
|
||||||
public void failure(ActionInvocation invocation,
|
public void failure(ActionInvocation invocation,
|
||||||
|
@ -224,17 +231,16 @@ public class PlayService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateNotification() {
|
private void updateNotification() {
|
||||||
|
if (mShowNotification) {
|
||||||
new CreateNotificationTask().execute(mPlaylist.get(mCurrentTrack)
|
new CreateNotificationTask().execute(mPlaylist.get(mCurrentTrack)
|
||||||
.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class));
|
.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends 'play' signal to current renderer.
|
* Sends 'play' signal to current renderer.
|
||||||
*/
|
*/
|
||||||
public void play() {
|
public void play() {
|
||||||
if (mPlaylist.size() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
updateNotification();
|
updateNotification();
|
||||||
mUpnpService.getControlPoint().execute(
|
mUpnpService.getControlPoint().execute(
|
||||||
new Play(mAvTransportService) {
|
new Play(mAvTransportService) {
|
||||||
|
@ -262,17 +268,28 @@ public class PlayService extends Service {
|
||||||
UpnpResponse operation, String defaultMessage) {
|
UpnpResponse operation, String defaultMessage) {
|
||||||
Log.w(TAG, "Pause failed, trying stop: " + defaultMessage);
|
Log.w(TAG, "Pause failed, trying stop: " + defaultMessage);
|
||||||
// Sometimes stop works even though pause does not.
|
// Sometimes stop works even though pause does not.
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends 'stop' signal to current renderer.
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
mManuallyStopped.set(true);
|
||||||
|
|
||||||
mUpnpService.getControlPoint().execute(
|
mUpnpService.getControlPoint().execute(
|
||||||
new Stop(mAvTransportService) {
|
new Stop(mAvTransportService) {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
@Override
|
@Override
|
||||||
public void failure(ActionInvocation invocation,
|
public void failure( ActionInvocation invocation,
|
||||||
UpnpResponse operation, String defaultMessage) {
|
UpnpResponse operation, String defaultMessage) {
|
||||||
Log.w(TAG, "Stop failed: " + defaultMessage);
|
Log.w(TAG, "Stop failed: " + defaultMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRenderer(Device<?, ?, ?> renderer) {
|
public void setRenderer(Device<?, ?, ?> renderer) {
|
||||||
|
@ -390,4 +407,8 @@ public class PlayService extends Service {
|
||||||
return mCurrentTrack;
|
return mCurrentTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setShowNotification(boolean value) {
|
||||||
|
mShowNotification = value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class UpnpController {
|
||||||
/**
|
/**
|
||||||
* Returns a device service by name for direct queries.
|
* Returns a device service by name for direct queries.
|
||||||
*/
|
*/
|
||||||
public Service<?, ?> getService(Device<?, ?, ?> device, String name) {
|
public static Service<?, ?> getService(Device<?, ?, ?> device, String name) {
|
||||||
return device.findService(
|
return device.findService(
|
||||||
new ServiceType("schemas-upnp-org", name));
|
new ServiceType("schemas-upnp-org", name));
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,6 @@ public class UpnpPlayer extends UpnpController {
|
||||||
if (newVolume < mMinVolume) newVolume = mMinVolume;
|
if (newVolume < mMinVolume) newVolume = mMinVolume;
|
||||||
|
|
||||||
mCurrentVolume = newVolume;
|
mCurrentVolume = newVolume;
|
||||||
Log.d(TAG, "volume: " + Integer.toString(mCurrentVolume));
|
|
||||||
mUpnpService.getControlPoint().execute(
|
mUpnpService.getControlPoint().execute(
|
||||||
new SetVolume(getService("RenderingControl"), newVolume) {
|
new SetVolume(getService("RenderingControl"), newVolume) {
|
||||||
|
|
||||||
|
@ -156,7 +155,6 @@ public class UpnpPlayer extends UpnpController {
|
||||||
else {
|
else {
|
||||||
mMinVolume = 0;
|
mMinVolume = 0;
|
||||||
mMaxVolume = 100;
|
mMaxVolume = 100;
|
||||||
mVolumeStep = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mUpnpService.getControlPoint().execute(
|
mUpnpService.getControlPoint().execute(
|
||||||
|
@ -203,7 +201,9 @@ public class UpnpPlayer extends UpnpController {
|
||||||
* Returns the service that handles actual playback.
|
* Returns the service that handles actual playback.
|
||||||
*/
|
*/
|
||||||
public PlayService getPlayService() {
|
public PlayService getPlayService() {
|
||||||
return mPlayService.getService();
|
return (mPlayService != null)
|
||||||
|
? mPlayService.getService()
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue