/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2008 A.Brochard
*
* This program 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; version 2
* of the License only.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
package net.pms.network;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import net.pms.PMS;
import net.pms.configuration.DeviceConfiguration;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.dlna.DLNAResource;
import static net.pms.dlna.DLNAResource.Temp;
import net.pms.util.BasicPlayer;
import net.pms.util.StringUtil;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.fourthline.cling.model.meta.Device;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helper class to handle the UPnP traffic that makes UMS discoverable by
* other clients.
* See http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf
* and http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1-AnnexA.pdf
* for the specifications.
*/
public class UPNPHelper extends UPNPControl {
// Logger instance to write messages to the logs.
private static final Logger LOGGER = LoggerFactory.getLogger(UPNPHelper.class);
// Carriage return and line feed.
private static final String CRLF = "\r\n";
// The Constant ALIVE.
private static final String ALIVE = "ssdp:alive";
/**
* IPv4 Multicast channel reserved for SSDP by Internet Assigned Numbers Authority (IANA).
* MUST be 239.255.255.250.
*/
private static final String IPV4_UPNP_HOST = "239.255.255.250";
/**
* Multicast channel reserved for SSDP by Internet Assigned Numbers Authority (IANA).
* MUST be 1900.
*/
private static final int UPNP_PORT = 1900;
// The Constant BYEBYE.
private static final String BYEBYE = "ssdp:byebye";
private static final String[] NT_LIST = {
"upnp:rootdevice",
"urn:schemas-upnp-org:device:MediaServer:1",
"urn:schemas-upnp-org:service:ContentDirectory:1",
"urn:schemas-upnp-org:service:ConnectionManager:1",
PMS.get().usn(),
"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
};
private static final String[] ST_LIST = {
"urn:schemas-upnp-org:device:MediaRenderer:1",
"urn:schemas-upnp-org:device:Basic:1"
};
// The listener.
private static Thread listenerThread;
// The alive thread.
private static Thread aliveThread;
private static final PmsConfiguration configuration = PMS.getConfiguration();
private static final UPNPHelper instance = new UPNPHelper();
private static PlayerControlHandler httpControlHandler;
/**
* This utility class is not meant to be instantiated.
*/
private UPNPHelper() {
rendererMap = new DeviceMap<>(DeviceConfiguration.class);
}
public static UPNPHelper getInstance() {
return instance;
}
@Override
public void init() {
if (configuration.isUpnpEnabled()) {
super.init();
}
getHttpControlHandler();
}
public static PlayerControlHandler getHttpControlHandler() {
if (
httpControlHandler == null &&
PMS.get().getWebServer() != null &&
!"false".equals(configuration.getBumpAddress().toLowerCase())
) {
httpControlHandler = new PlayerControlHandler(PMS.get().getWebInterface());
LOGGER.debug("Attached http player control handler to web server");
}
return httpControlHandler;
}
private static String lastSearch = null;
/**
* Send UPnP discovery search message to discover devices of interest on
* the network.
*
* @param host The multicast channel
* @param port The multicast port
* @param st The search target string
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void sendDiscover(String host, int port, String st) throws IOException {
String usn = PMS.get().usn();
String serverHost = PMS.get().getServer().getHost();
int serverPort = PMS.get().getServer().getPort();
SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
if (st.equals(usn)) {
usn = "";
} else {
usn += "::";
}
StringBuilder discovery = new StringBuilder();
discovery.append("HTTP/1.1 200 OK").append(CRLF);
discovery.append("CACHE-CONTROL: max-age=1800").append(CRLF);
discovery.append("DATE: ").append(sdf.format(new Date(System.currentTimeMillis()))).append(" GMT").append(CRLF);
discovery.append("LOCATION: http://").append(serverHost).append(':').append(serverPort).append("/description/fetch").append(CRLF);
discovery.append("SERVER: ").append(PMS.get().getServerName()).append(CRLF);
discovery.append("ST: ").append(st).append(CRLF);
discovery.append("EXT: ").append(CRLF);
discovery.append("USN: ").append(usn).append(st).append(CRLF);
discovery.append("Content-Length: 0").append(CRLF).append(CRLF);
String msg = discovery.toString();
if (LOGGER.isTraceEnabled()) {
if (st.equals(lastSearch)) {
LOGGER.trace("Resending last discovery [" + host + ":" + port + "]");
} else {
LOGGER.trace("Sending discovery [" + host + ":" + port + "]: " + StringUtils.replace(msg, CRLF, "<CRLF>"));
}
}
sendReply(host, port, msg);
for (String ST: ST_LIST) {
discovery = new StringBuilder();
discovery.append("M-SEARCH * HTTP/1.1").append(CRLF);
discovery.append("ST: ").append(ST).append(CRLF);
discovery.append("HOST: ").append(IPV4_UPNP_HOST).append(':').append(UPNP_PORT).append(CRLF);
discovery.append("MX: 3").append(CRLF);
discovery.append("MAN: \"ssdp:discover\"").append(CRLF).append(CRLF);
msg = discovery.toString();
sendReply(host, port, msg);
}
lastSearch = st;
}
/**
* Send reply.
*
* @param host the host
* @param port the port
* @param msg the msg
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void sendReply(String host, int port, String msg) {
try (DatagramSocket datagramSocket = new DatagramSocket()) {
InetAddress inetAddr = InetAddress.getByName(host);
DatagramPacket dgmPacket = new DatagramPacket(msg.getBytes(), msg.length(), inetAddr, port);
datagramSocket.send(dgmPacket);
} catch (Exception e) {
LOGGER.info(e.getMessage());
LOGGER.debug("Error sending reply", e);
}
}
/**
* Send alive.
*/
public static void sendAlive() {
LOGGER.debug("Sending ALIVE...");
MulticastSocket multicastSocket = null;
try {
multicastSocket = getNewMulticastSocket();
InetAddress upnpAddress = getUPNPAddress();
multicastSocket.joinGroup(upnpAddress);
for (String NT: NT_LIST) {
sendMessage(multicastSocket, NT, ALIVE);
}
} catch (IOException e) {
LOGGER.debug("Error sending ALIVE message", e);
} finally {
if (multicastSocket != null) {
// Clean up the multicast socket nicely
try {
InetAddress upnpAddress = getUPNPAddress();
multicastSocket.leaveGroup(upnpAddress);
} catch (IOException e) {
}
multicastSocket.disconnect();
multicastSocket.close();
}
}
}
static boolean multicastLog = true;
/**
* Gets the new multicast socket.
*
* @return the new multicast socket
* @throws IOException Signals that an I/O exception has occurred.
*/
private static MulticastSocket getNewMulticastSocket() throws IOException {
NetworkInterface networkInterface = NetworkConfiguration.getInstance().getNetworkInterfaceByServerName();
if (networkInterface == null) {
try {
networkInterface = PMS.get().getServer().getNetworkInterface();
} catch (NullPointerException e) {
LOGGER.debug("Couldn't get server network interface. Trying again in 5 seconds.");
try {
Thread.sleep(5000);
} catch (InterruptedException e2) { }
try {
networkInterface = PMS.get().getServer().getNetworkInterface();
} catch (NullPointerException e3) {
LOGGER.debug("Couldn't get server network interface.");
}
}
}
if (networkInterface == null) {
throw new IOException("No usable network interface found for UPnP multicast");
}
List<InetAddress> usableAddresses = new ArrayList<>();
List<InetAddress> networkInterfaceAddresses = Collections.list(networkInterface.getInetAddresses());
for (InetAddress inetAddress : networkInterfaceAddresses) {
if (inetAddress != null && inetAddress instanceof Inet4Address && !inetAddress.isLoopbackAddress()) {
usableAddresses.add(inetAddress);
}
}
if (usableAddresses.isEmpty()) {
throw new IOException("No usable addresses found for UPnP multicast");
}
InetSocketAddress localAddress = new InetSocketAddress(usableAddresses.get(0), 0);
MulticastSocket ssdpSocket = new MulticastSocket(localAddress);
ssdpSocket.setReuseAddress(true);
ssdpSocket.setTimeToLive(32);
if (multicastLog) {
LOGGER.trace("Sending message from multicast socket on network interface: " + ssdpSocket.getNetworkInterface());
LOGGER.trace("Multicast socket is on interface: " + ssdpSocket.getInterface());
LOGGER.trace("Socket Timeout: " + ssdpSocket.getSoTimeout());
LOGGER.trace("Socket TTL: " + ssdpSocket.getTimeToLive());
multicastLog = false;
}
return ssdpSocket;
}
/**
* Send the UPnP BYEBYE message.
*/
public static void sendByeBye() {
LOGGER.debug("Sending BYEBYE...");
MulticastSocket multicastSocket = null;
try {
multicastSocket = getNewMulticastSocket();
InetAddress upnpAddress = getUPNPAddress();
multicastSocket.joinGroup(upnpAddress);
for (String NT: NT_LIST) {
sendMessage(multicastSocket, NT, BYEBYE, true);
}
} catch (IOException e) {
LOGGER.debug("Error sending BYEBYE message", e);
} finally {
if (multicastSocket != null) {
// Clean up the multicast socket nicely
try {
InetAddress upnpAddress = getUPNPAddress();
multicastSocket.leaveGroup(upnpAddress);
} catch (IOException e) {
}
multicastSocket.disconnect();
multicastSocket.close();
}
}
}
/**
* Utility method to call {@link Thread#sleep(long)} without having to
* catch the InterruptedException.
*
* @param delay the delay
*/
public static void sleep(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) { }
}
/**
* Send the provided message to the socket three times.
*
* @see #sendMessage(java.net.DatagramSocket, java.lang.String, java.lang.String, boolean)
* @param socket the socket
* @param nt the nt
* @param message the message
* @throws IOException
*/
private static void sendMessage(DatagramSocket socket, String nt, String message) throws IOException {
sendMessage(socket, nt, message, false);
}
/**
* Send the provided message to the socket.
*
* @param socket the socket
* @param nt the nt
* @param message the message
* @param sendOnce send the message only once
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void sendMessage(DatagramSocket socket, String nt, String message, boolean sendOnce) throws IOException {
String msg = buildMsg(nt, message);
Random rand = new Random();
// LOGGER.trace( "Sending this SSDP packet: " + CRLF + StringUtils.replace(msg, CRLF, "<CRLF>")));
InetAddress upnpAddress = getUPNPAddress();
DatagramPacket ssdpPacket = new DatagramPacket(msg.getBytes(), msg.length(), upnpAddress, UPNP_PORT);
/**
* Requirement [7.2.4.1]: UPnP endpoints (devices and control points) should
* wait a random amount of time, between 0 and 100 milliseconds after acquiring
* a new IP address, before sending advertisements or initiating searches on a
* new IP interface.
*/
sleep(rand.nextInt(101));
socket.send(ssdpPacket);
// Send the message three times as recommended by the standard
if (!sendOnce) {
sleep(100);
socket.send(ssdpPacket);
sleep(100);
socket.send(ssdpPacket);
}
}
private static int ALIVE_delay = 10000;
/**
* Starts up two threads: one to broadcast UPnP ALIVE messages and another
* to listen for responses.
*
* @throws IOException Signals that an I/O exception has occurred.
*/
public static void listen() throws IOException {
Runnable rAlive = new Runnable() {
@Override
public void run() {
while (true) {
sleep(ALIVE_delay);
sendAlive();
// If getAliveDelay is 0, there is no custom alive delay
if (configuration.getAliveDelay() == 0) {
if (PMS.get().getFoundRenderers().size() > 0) {
ALIVE_delay = 30000;
} else {
ALIVE_delay = 10000;
}
} else {
ALIVE_delay = configuration.getAliveDelay();
}
}
}
};
aliveThread = new Thread(rAlive, "UPNP-AliveMessageSender");
aliveThread.start();
Runnable r = new Runnable() {
@Override
public void run() {
boolean bindErrorReported = false;
while (true) {
MulticastSocket multicastSocket = null;
try {
// Use configurable source port as per http://code.google.com/p/ps3mediaserver/issues/detail?id=1166
multicastSocket = new MulticastSocket(configuration.getUpnpPort());
if (bindErrorReported) {
LOGGER.warn("Finally, acquiring port " + configuration.getUpnpPort() + " was successful!");
}
NetworkInterface ni = NetworkConfiguration.getInstance().getNetworkInterfaceByServerName();
try {
/**
* Setting the network interface will throw a SocketException on Mac OS X
* with Java 1.6.0_45 or higher, but if we don't do it some Windows
* configurations will not listen at all.
*/
if (ni != null) {
multicastSocket.setNetworkInterface(ni);
LOGGER.trace("Setting multicast network interface: {}", ni);
} else if (PMS.get().getServer().getNetworkInterface() != null) {
multicastSocket.setNetworkInterface(PMS.get().getServer().getNetworkInterface());
LOGGER.trace("Setting multicast network interface: {}", PMS.get().getServer().getNetworkInterface());
}
} catch (SocketException e) {
// Not setting the network interface will work just fine on Mac OS X.
}
multicastSocket.setTimeToLive(4);
multicastSocket.setReuseAddress(true);
InetAddress upnpAddress = getUPNPAddress();
multicastSocket.joinGroup(upnpAddress);
final int M_SEARCH = 1;
final int NOTIFY = 2;
InetAddress lastAddress = null;
int lastPacketType = 0;
while (true) {
byte[] buf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(buf, buf.length);
multicastSocket.receive(receivePacket);
String s = new String(receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
InetAddress address = receivePacket.getAddress();
int packetType = s.startsWith("M-SEARCH") ? M_SEARCH : s.startsWith("NOTIFY") ? NOTIFY : 0;
boolean redundant = address.equals(lastAddress) && packetType == lastPacketType;
if (packetType == M_SEARCH) {
if (configuration.getIpFiltering().allowed(address)) {
String remoteAddr = address.getHostAddress();
int remotePort = receivePacket.getPort();
if (!redundant && LOGGER.isTraceEnabled()) {
LOGGER.trace("Received a M-SEARCH from [" + remoteAddr + ":" + remotePort + "]: " + s);
}
if (StringUtils.indexOf(s, "urn:schemas-upnp-org:service:ContentDirectory:1") > 0) {
sendDiscover(remoteAddr, remotePort, "urn:schemas-upnp-org:service:ContentDirectory:1");
}
if (StringUtils.indexOf(s, "upnp:rootdevice") > 0) {
sendDiscover(remoteAddr, remotePort, "upnp:rootdevice");
}
if (
StringUtils.indexOf(s, "urn:schemas-upnp-org:device:MediaServer:1") > 0 ||
StringUtils.indexOf(s, "ssdp:all") > 0
) {
sendDiscover(remoteAddr, remotePort, "urn:schemas-upnp-org:device:MediaServer:1");
}
if (StringUtils.indexOf(s, PMS.get().usn()) > 0) {
sendDiscover(remoteAddr, remotePort, PMS.get().usn());
}
}
// Don't log redundant notify messages
} else if (packetType == NOTIFY && !redundant && LOGGER.isTraceEnabled()) {
LOGGER.trace("Received a NOTIFY from [{}:{}]", address.getHostAddress(), receivePacket.getPort());
}
lastAddress = address;
lastPacketType = packetType;
}
} catch (BindException e) {
if (!bindErrorReported) {
LOGGER.error("Unable to bind to " + configuration.getUpnpPort()
+ ", which means that UMS will not automatically appear on your renderer! "
+ "This usually means that another program occupies the port. Please "
+ "stop the other program and free up the port. "
+ "UMS will keep trying to bind to it...[" + e.getMessage() + "]");
}
bindErrorReported = true;
sleep(5000);
} catch (IOException e) {
LOGGER.error("UPnP network exception: ", e.getMessage());
LOGGER.trace("", e);
sleep(1000);
} finally {
if (multicastSocket != null) {
// Clean up the multicast socket nicely
try {
InetAddress upnpAddress = getUPNPAddress();
multicastSocket.leaveGroup(upnpAddress);
} catch (IOException e) {
LOGGER.trace("Final UPnP network exception: ", e.getMessage());
LOGGER.trace("", e);
}
multicastSocket.disconnect();
multicastSocket.close();
}
}
}
}
};
listenerThread = new Thread(r, "UPNPHelper");
listenerThread.start();
}
/**
* Shut down the threads that send ALIVE messages and listen to responses.
*/
public static void shutDownListener() {
instance.shutdown();
if (listenerThread != null) {
listenerThread.interrupt();
}
if (aliveThread != null) {
aliveThread.interrupt();
}
}
/**
* Builds a UPnP message string based on a message.
*
* @param nt the nt
* @param message the message
* @return the string
*/
private static String buildMsg(String nt, String message) {
StringBuilder sb = new StringBuilder();
sb.append("NOTIFY * HTTP/1.1").append(CRLF);
sb.append("HOST: ").append(IPV4_UPNP_HOST).append(':').append(UPNP_PORT).append(CRLF);
sb.append("NT: ").append(nt).append(CRLF);
sb.append("NTS: ").append(message).append(CRLF);
if (message.equals(ALIVE)) {
sb.append("LOCATION: http://").append(PMS.get().getServer().getHost()).append(':').append(PMS.get().getServer().getPort()).append("/description/fetch").append(CRLF);
}
sb.append("USN: ").append(PMS.get().usn());
if (!nt.equals(PMS.get().usn())) {
sb.append("::").append(nt);
}
sb.append(CRLF);
if (message.equals(ALIVE)) {
sb.append("CACHE-CONTROL: max-age=1800").append(CRLF);
sb.append("SERVER: ").append(PMS.get().getServerName()).append(CRLF);
}
// Sony devices like PS3 and PS4 need this extra linebreak
sb.append(CRLF);
return sb.toString();
}
/**
* Gets the UPnP address.
*
* @return the UPnP address
* @throws IOException Signals that an I/O exception has occurred.
*/
private static InetAddress getUPNPAddress() throws IOException {
return InetAddress.getByName(IPV4_UPNP_HOST);
}
public void addRenderer(DeviceConfiguration d) {
if (d.uuid != null) {
rendererMap.put(d.uuid, "0", d);
}
}
public void removeRenderer(RendererConfiguration d) {
if (d.uuid != null) {
rendererMap.remove(d.uuid);
}
}
public static boolean activate(String uuid) {
if (! rendererMap.containsKey(uuid)) {
LOGGER.debug("Activating upnp service for {}", uuid);
return getInstance().addRenderer(uuid);
}
return true;
}
@Override
protected boolean isBlocked(String uuid) {
int mode = DeviceConfiguration.getDeviceUpnpMode(uuid, true);
if (mode != RendererConfiguration.ALLOW) {
LOGGER.debug("Upnp service is {} for {}", RendererConfiguration.getUpnpModeString(mode), uuid);
return true;
}
return false;
}
@Override
protected Renderer rendererFound(Device d, String uuid) {
// Create or retrieve an instance
try {
InetAddress socket = InetAddress.getByName(getURL(d).getHost());
DeviceConfiguration r = (DeviceConfiguration) RendererConfiguration.getRendererConfigurationBySocketAddress(socket);
RendererConfiguration ref = configuration.isRendererForceDefault() ?
null : RendererConfiguration.getRendererConfigurationByUPNPDetails(getDeviceDetailsString(d));
if (r != null && ! r.isUpnpAllowed()) {
LOGGER.debug("Upnp service is {} for \"{}\"", r.getUpnpModeString(), r);
return null;
} else if (r == null && ref != null && ! ref.isUpnpAllowed()) {
LOGGER.debug("Upnp service is {} for {} devices", ref.getUpnpModeString(), ref);
return null;
}
// FIXME: when UpnpDetailsSearch is missing from the conf a upnp-advertising
// renderer could register twice if the http server sees it first
boolean distinct = r != null && StringUtils.isNotBlank(r.getUUID()) && ! uuid.equals(r.getUUID());
if (! distinct && r != null && (r.matchUPNPDetails(getDeviceDetailsString(d)) || ! r.loaded)) {
// Already seen by the http server
if (
ref != null &&
!ref.getUpnpDetailsString().equals(r.getUpnpDetailsString()) &&
ref.getLoadingPriority() >= r.getLoadingPriority()
) {
// The upnp-matched reference conf is different from the previous
// http-matched conf and has equal or higher priority, so update.
LOGGER.debug("Switching to preferred renderer: " + ref);
r.inherit(ref);
}
// Update if we have a custom configuration for this uuid
r.setUUID(uuid);
// Make sure it's mapped
rendererMap.put(uuid, "0", r);
r.details = getDeviceDetails(d);
// Update gui
PMS.get().updateRenderer(r);
LOGGER.debug("Found upnp service for \"{}\" with dlna details: {}", r, r.details);
} else {
// It's brand new
r = (DeviceConfiguration) rendererMap.get(uuid, "0");
if (ref != null) {
r.inherit(ref);
} else {
// It's unrecognized: temporarily assign the default renderer but mark it as unloaded
// so actual recognition can happen later once the http server receives a request.
// This is to allow initiation of upnp playback before http recognition has occurred.
r.inherit(r.getDefaultConf());
r.loaded = false;
LOGGER.debug("Marking upnp renderer \"{}\" at {} as unrecognized", r, socket);
}
if (r.associateIP(socket)) {
r.details = getDeviceDetails(d);
PMS.get().setRendererFound(r);
LOGGER.debug("New renderer found: \"{}\" with dlna details: {}", r, r.details);
}
}
return r;
} catch (Exception e) {
LOGGER.debug("Error initializing device " + getFriendlyName(d) + ": " + e);
e.printStackTrace();
}
return null;
}
public static InetAddress getAddress(String uuid) {
try {
return InetAddress.getByName(getURL(getDevice(uuid)).getHost());
} catch (Exception e) {
}
return null;
}
public static boolean hasRenderer(int type) {
for (Map<String, Renderer> item : (Collection<Map<String, Renderer>>) rendererMap.values()) {
Renderer r = item.get("0");
if ((r.controls & type) != 0) {
return true;
}
}
return false;
}
public static List<RendererConfiguration> getRenderers(int type) {
ArrayList<RendererConfiguration> renderers = new ArrayList<>();
for (Map<String, Renderer> item : (Collection<Map<String, Renderer>>) rendererMap.values()) {
Renderer r = item.get("0");
if (r.active && (r.controls & type) != 0) {
renderers.add((RendererConfiguration) r);
}
}
return renderers;
}
@Override
protected void rendererReady(String uuid) {
RendererConfiguration r = RendererConfiguration.getRendererConfigurationByUUID(uuid);
if(r != null) {
r.getPlayer();
}
}
public static void play(String uri, String name, DeviceConfiguration r) {
DLNAResource d = DLNAResource.getValidResource(uri, name, r);
if (d != null) {
play(d, r);
}
}
public static void play(DLNAResource d, DeviceConfiguration r) {
DLNAResource d1 = d.getParent() == null ? Temp.add(d) : d;
if (d1 != null) {
Device dev = getDevice(r.getUUID());
String id = r.getInstanceID();
setAVTransportURI(dev, id, d1.getURL(""), r.isPushMetadata() ? d1.getDidlString(r) : null);
play(dev, id);
}
}
// A logical player to manage upnp playback
public static class Player extends BasicPlayer.Logical {
protected Device dev;
protected String uuid;
protected String instanceID;
protected Map<String, String> data;
protected String lasturi;
private boolean ignoreUpnpDuration;
public Player(DeviceConfiguration renderer) {
super(renderer);
uuid = renderer.getUUID();
instanceID = renderer.getInstanceID();
dev = getDevice(uuid);
data = rendererMap.get(uuid, instanceID).connect(this);
lasturi = null;
ignoreUpnpDuration = false;
LOGGER.debug("Created upnp player for " + renderer.getRendererName());
refresh();
}
@Override
public void setURI(String uri, String metadata) {
Playlist.Item item = resolveURI(uri, metadata);
if (item != null) {
if (item.name != null) {
state.name = item.name;
}
UPNPControl.setAVTransportURI(dev, instanceID, item.uri, renderer.isPushMetadata() ? item.metadata : null);
}
}
@Override
public void play() {
UPNPControl.play(dev, instanceID);
}
@Override
public void pause() {
UPNPControl.pause(dev, instanceID);
}
@Override
public void stop() {
UPNPControl.stop(dev, instanceID);
}
@Override
public void forward() {
UPNPControl.seek(dev, instanceID, REL_TIME, jump(60));
}
@Override
public void rewind() {
UPNPControl.seek(dev, instanceID, REL_TIME, jump(-60));
}
@Override
public void mute() {
UPNPControl.setMute(dev, instanceID, !state.mute);
}
@Override
public void setVolume(int volume) {
UPNPControl.setVolume(dev, instanceID, volume * maxVol / 100);
}
@Override
public void actionPerformed(final ActionEvent e) {
if (renderer.isUpnpConnected()) {
refresh();
} else if (state.playback != STOPPED) {
reset();
}
}
public void refresh() {
String s = data.get("TransportState");
state.playback = "STOPPED".equals(s) ? STOPPED :
"PLAYING".equals(s) ? PLAYING :
"PAUSED_PLAYBACK".equals(s) ? PAUSED: -1;
state.mute = !"0".equals(data.get("Mute"));
s = data.get("Volume");
state.volume = s == null ? 0 : (Integer.valueOf(s) * 100 / maxVol);
state.position = data.get("RelTime");
if (!ignoreUpnpDuration) {
state.duration = data.get("CurrentMediaDuration");
}
state.uri = data.get("AVTransportURI");
state.metadata = data.get("AVTransportURIMetaData");
// update playlist only if uri has changed
if (!StringUtils.isBlank(state.uri) && !state.uri.equals(lasturi)) {
playlist.set(state.uri, null, state.metadata);
}
lasturi = state.uri;
alert();
}
@Override
public void start() {
DLNAResource d = renderer.getPlayingRes();
state.name = d.getDisplayName();
if (d.getMedia() != null) {
String duration = d.getMedia().getDurationString();
ignoreUpnpDuration = !StringUtil.isZeroTime(duration);
if (ignoreUpnpDuration) {
state.duration = StringUtil.shortTime(d.getMedia().getDurationString(), 4);
}
}
}
@Override
public void close() {
rendererMap.get(uuid, instanceID).disconnect(this);
super.close();
}
public String jump(double seconds) {
double t = StringUtil.convertStringToTime(state.position) + seconds;
return t > 0 ? StringUtil.convertTimeToString(t, "%02d:%02d:%02.0f") : "00:00:00";
}
}
public static String unescape(String s) throws UnsupportedEncodingException {
return StringEscapeUtils.unescapeXml(StringEscapeUtils.unescapeHtml4(URLDecoder.decode(s, "UTF-8")));
}
}