/* * Copyright Ericsson AB 2011-2014. All Rights Reserved. * * The contents of this file are subject to the Lesser GNU Public License, * (the "License"), either version 2.1 of the License, or * (at your option) any later version.; you may not use this file except in * compliance with the License. You should have received a copy of the * License along with this software. If not, it can be * retrieved online at https://www.gnu.org/licenses/lgpl.html. Moreover * it could also be requested from Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO * WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. * EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR * OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE * LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, * YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. * * IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING * WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR * REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR * DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL * DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY * (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED * INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE * OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH * HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. * */ package com.ericsson.deviceaccess.upnp; import com.ericsson.common.util.LegacyUtil; import com.ericsson.common.util.StringUtil; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.Dictionary; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.service.upnp.UPnPAction; import org.osgi.service.upnp.UPnPDevice; import org.osgi.service.upnp.UPnPException; import org.osgi.service.upnp.UPnPService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class UPnPUtil { private static final Logger logger = LoggerFactory.getLogger(UPnPDeviceAgent.class); public static final String DEVICE_TYPE_MEDIA_RENDERER = "urn:schemas-upnp-org:device:MediaRenderer"; public static final String DEVICE_TYPE_MEDIA_SERVER = "urn:schemas-upnp-org:device:MediaServer"; public static final String DEVICE_TYPE_BINARY_LIGHT = "urn:schemas-upnp-org:device:BinaryLight"; // public static final String SERVICE_ID_RENDERING_CONTROL = // "urn:upnp-org:serviceId:RenderingControlServiceID"; public static final String SRV_RENDERING_CONTROL = "RenderingControl"; public static final String SRV_AV_TRANSPORT = "AVTransport"; public static final String SRV_CONTENT_DIRECTORY = "ContentDirectory"; private static final String BROWSE_ACTION = "Browse"; private static final String RESULT = "Result"; static final String HEADER_CONTENT_TYPE = "Content-Type"; private static final Pattern varPattern = Pattern.compile("<(\\w+)\\s.*val=\"(.*)\".*/"); public static String getCurrentMediaUri(UPnPDevice dev) throws Exception { Properties args = new Properties(); args.put("InstanceID", "0"); Dictionary result = getUPnPAction(dev, "GetMediaInfo").invoke(args); if (result != null) { return (String) result.get("CurrentURI"); } throw new UPnPException(UPnPException.DEVICE_INTERNAL_ERROR, "Could not get result for action"); } public static void stopMedia(UPnPDevice dev) throws Exception { info("Going to stop playing on " + getFriendlyName(dev)); Properties args = new Properties(); args.put("InstanceID", "0"); getUPnPAction(dev, "Stop").invoke(args); } public static void pauseMedia(UPnPDevice dev) throws Exception { Properties args = new Properties(); args.put("InstanceID", "0"); getUPnPAction(dev, "Pause").invoke(args); } public static void resumeMedia(UPnPDevice dev) throws Exception { Properties args = new Properties(); args.put("InstanceID", "0"); args.put("Speed", "1"); getUPnPAction(dev, "Play").invoke(args); } public static String playMedia(UPnPDevice dev, String url) throws Exception { return playMedia(dev, url, ""); } public static String playMedia(UPnPDevice dev, String url, String title) throws Exception { info("Going to play " + url + " on " + UPnPUtil.getFriendlyName(dev)); url = url.trim(); Properties args = new Properties(); args.put("InstanceID", "0"); args.put("CurrentURI", url); if (title == null) { title = ""; } String contentType = getContentType(url); args.put("CurrentURIMetaData", getCurrentURIMetadata(url, title, contentType)); debug(args.toString()); getUPnPAction(dev, "SetAVTransportURI").invoke(args); debug("SetAVTransportURI successfully invoked"); args = new Properties(); args.put("InstanceID", "0"); args.put("Speed", "1"); getUPnPAction(dev, "Play").invoke(args); debug("Play successfully invoked"); return contentType; } private static String getContentType(String url) throws UPnPUtilException { String contentType; try { contentType = HttpClient.getHeader(url, HEADER_CONTENT_TYPE); if (contentType == null || !contentType.contains("/")) { throw new UPnPUtilException(404, "Could not determine content type"); } debug("Content type is " + contentType); if (contentType.contains(";")) { /* * remove optional data such as character encoding. * e.g. video/mp4;UTF-8 */ contentType = contentType.substring(0, contentType.indexOf(';')); } } catch (IOException e) { throw new UPnPUtilException(500, e.getMessage()); } /* * Workaround for ustream mp4 file which returns audio/mp4 * 20100917 Kenta */ if (url.contains("ustream")) { contentType = "video/mp4"; } return contentType; } protected static String getCurrentURIMetadata(String url, String title, String contentType) throws UPnPUtilException { String itemType = contentType.substring(0, contentType.indexOf('/')); String md = "<DIDL-Lite xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\">"; md += "<item>"; md += "<dc:title>" + title + "</dc:title>"; md += "<upnp:class>object.item." + itemType + "Item</upnp:class>"; md += "<res protocolInfo=\"http-get:*:" + contentType + ":*\">"; md += url; md += "</res>"; md += "</item>"; md += "</DIDL-Lite>"; return md; } public static void setVolume(UPnPDevice dev, String volume) throws Exception { Properties args = new Properties(); args.put("InstanceID", "0"); args.put("Channel", "Master"); args.put("DesiredVolume", volume); getUPnPAction(dev, "SetVolume").invoke(args); } public static Map<String, Object> browse(UPnPDevice dev, Map<String, Object> givenArgs) throws UPnPException { UPnPAction action = getUPnPAction(dev, BROWSE_ACTION); if (action == null) { throw new UPnPException(404, "No such action supported by " + getFriendlyName(dev)); } Map<String, Object> args = getDefaultBrowseArguments(); for (String argName : action.getInputArgumentNames()) { if (givenArgs.containsKey(argName)) { debug("Setting " + argName + " to " + givenArgs.get(argName)); args.put(argName, givenArgs.get(argName)); } } try { debug(args.toString()); return LegacyUtil.toMap(action.invoke(LegacyUtil.toDictionary(args))); } catch (Exception e) { String msg = ("Failed to invoke UPnP action." + e); error(msg); throw new UPnPException(500, msg); } } private static Map<String, Object> getDefaultBrowseArguments() { Map<String, Object> args = new HashMap<>(); args.put("ObjectID", 0); args.put("BrowseFlag", "BrowseDirectChildren"); args.put("Filter", "*"); args.put("StartingIndex", 0); args.put("RequestedCount", 0); args.put("SortCriteria", ""); return args; } public static Map<String, String> parseLastChangeEvent(String value) { Map<String, String> result = new HashMap<>(); for (String tag : value.split(">")) { Matcher matcher = varPattern.matcher(tag); if (matcher.find()) { result.put(matcher.group(1), matcher.group(2)); } } return result; } private static UPnPAction getUPnPAction(UPnPDevice device, String actionName) throws UPnPException { for (UPnPService service : device.getServices()) { UPnPAction action = service.getAction(actionName); if (action != null) { return action; } } throw new UPnPException(UPnPException.INVALID_ACTION, "No such action supported " + actionName); } public static String getUDN(UPnPDevice dev) { return getProperty(dev, UPnPDevice.UDN); } public static String getDeviceType(UPnPDevice dev) { return getProperty(dev, UPnPDevice.TYPE); } public static String getFriendlyName(UPnPDevice dev) { return getProperty(dev, UPnPDevice.FRIENDLY_NAME); } public static String getModelName(UPnPDevice dev) { return getProperty(dev, UPnPDevice.MODEL_NAME); } public static boolean isMediaRenderer(UPnPDevice dev) { String type = getDeviceType(dev); return type != null && type.startsWith(DEVICE_TYPE_MEDIA_RENDERER); } public static boolean isMediaServer(UPnPDevice dev) { String type = getDeviceType(dev); return type != null && type.startsWith(DEVICE_TYPE_MEDIA_SERVER); } public static boolean isDimmableLight(UPnPDevice dev) { String type = getDeviceType(dev); String model = getModelName(dev); return type != null && type.startsWith(DEVICE_TYPE_BINARY_LIGHT) && model != null && model.startsWith("Intel"); } private static String getProperty(UPnPDevice dev, String name) { if (dev != null) { String value = (String) dev.getDescriptions(null).get(name); debug("Property " + name + " = " + value); return value; } return null; } public static UPnPDevice getTargetDevice(String uuid, BundleContext context) throws UPnPUtilException { if (uuid == null) { String msg = "Device UUID is not specified"; throw new UPnPUtilException(400, msg); } UPnPDevice target; try { // String uuidFilter = "(" + UPnPDevice.UDN + "=" + uuid + ")"; for (ServiceReference<UPnPDevice> ref : context.getServiceReferences(UPnPDevice.class, null)) { UPnPDevice dev = context.getService(ref); if (StringUtil.looseEquals(uuid, dev.getDescriptions(null).get(UPnPDevice.UDN))) { return dev; } } } catch (InvalidSyntaxException e) { throw new UPnPUtilException(500, e.getMessage()); } String msg = "No such device found"; throw new UPnPUtilException(404, msg); } private static void debug(String msg) { if (logger != null) { logger.debug(msg); } } private static void info(String msg) { if (logger != null) { logger.info(msg); } } private static void error(String msg) { if (logger != null) { logger.error(msg); } } static public String[] parseServiceType(String serviceType) { return serviceType.split(":"); } private UPnPUtil() { } protected static class HttpClient { public static String getHeader(String urlStr, String header) throws IOException { URL url = new URL(urlStr); HttpURLConnection urlconn = (HttpURLConnection) url.openConnection(); urlconn.setRequestMethod("HEAD"); urlconn.setInstanceFollowRedirects(true); urlconn.connect(); String value = urlconn.getHeaderField(header); urlconn.disconnect(); return value; } private HttpClient() { } } }