diff --git a/README.md b/README.md index e96f2ff..b2eeda2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # PipeCast -PipeCast is a library for 'casting' a stream to a device, like a Chromecast. Currently it only supports discovery. It'll be used by NewPipe. +PipeCast is a library for 'casting' a stream to a device, like a Chromecast. It's going to be used by NewPipe ## Supported protocols The following protocols are currently supported by PipeCast: -- UPnP +- UPnP A/V ## License diff --git a/src/main/java/org/schabi/newpipe/cast/Device.java b/src/main/java/org/schabi/newpipe/cast/Device.java index efc9f49..642b3c0 100644 --- a/src/main/java/org/schabi/newpipe/cast/Device.java +++ b/src/main/java/org/schabi/newpipe/cast/Device.java @@ -1,11 +1,17 @@ package org.schabi.newpipe.cast; +import java.io.IOException; + +import javax.xml.stream.XMLStreamException; + public abstract class Device { - protected final String location; + public final String location; public Device(String location) { this.location = location; } public abstract String getName(); + + public abstract void play(String url, String title, String creator, String mimeType, ItemClass itemClass) throws IOException, XMLStreamException; } diff --git a/src/main/java/org/schabi/newpipe/cast/ItemClass.java b/src/main/java/org/schabi/newpipe/cast/ItemClass.java new file mode 100644 index 0000000..ea28486 --- /dev/null +++ b/src/main/java/org/schabi/newpipe/cast/ItemClass.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.cast; + +public enum ItemClass { + MUSIC ("object.item.audioItem.musicTrack"), + MOVIE ("object.item.videoItem.movie"); + + public String upnpClass; + + ItemClass(String upnpClass) { + this.upnpClass = upnpClass; + } +} diff --git a/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDevice.java b/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDevice.java index 0061b83..e125b54 100644 --- a/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDevice.java +++ b/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDevice.java @@ -3,56 +3,184 @@ package org.schabi.newpipe.cast.protocols.upnp; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.StringReader; +import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.URL; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; import org.schabi.newpipe.cast.Device; +import org.schabi.newpipe.cast.ItemClass; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class UpnpDevice extends Device { private Document description; private Element device; + private URL controlUrl; - public UpnpDevice(String location) throws IOException { + public UpnpDevice(String location) throws IOException, ParserConfigurationException, SAXException { super(location); getDescription(); } - private void getDescription() throws IOException { - try { - URL url = new URL(location); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - BufferedReader input = new BufferedReader(new InputStreamReader(connection.getInputStream())); - String line; - StringBuilder response = new StringBuilder(); - while ((line = input.readLine()) != null) { - response.append(line); - } - input.close(); - - InputSource inputSource = new InputSource(new StringReader(response.toString())); - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - description = documentBuilder.parse(inputSource); - description.getDocumentElement().normalize(); - device = (Element) description.getDocumentElement().getElementsByTagName("device").item(0); - } catch (IOException | SAXException | ParserConfigurationException e) { - throw new IOException(e); + private void getDescription() throws IOException, ParserConfigurationException, SAXException { + URL url = new URL(location); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + BufferedReader input = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line; + StringBuilder response = new StringBuilder(); + while ((line = input.readLine()) != null) { + response.append(line); } + input.close(); + InputSource inputSource = new InputSource(new StringReader(response.toString())); + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + description = documentBuilder.parse(inputSource); + description.getDocumentElement().normalize(); + device = (Element) description.getDocumentElement().getElementsByTagName("device").item(0); + + URL baseUrl = new URL(description.getDocumentElement().getElementsByTagName("URLBase").item(0).getTextContent()); + + Element serviceList = (Element) device.getElementsByTagName("serviceList").item(0); + NodeList services = serviceList.getElementsByTagName("service"); + int servicesLength = services.getLength(); + for (int i = 0; i < servicesLength; i++) { + Element service = (Element) services.item(i); + if (service.getElementsByTagName("serviceType").item(0).getTextContent().equals("urn:schemas-upnp-org:service:AVTransport:1")) { + String serviceUrl = service.getElementsByTagName("controlURL").item(0).getTextContent(); + controlUrl = new URL(baseUrl, serviceUrl); + } + } } @Override public String getName() { return device.getElementsByTagName("friendlyName").item(0).getTextContent(); } + + private void play() throws IOException, XMLStreamException { + HttpURLConnection connection = (HttpURLConnection) controlUrl.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "text/xml;charset=utf-8"); + connection.setRequestProperty("Soapaction", "\"urn:schemas-upnp-org:service:AVTransport:1#Play\""); + OutputStream outputStream = connection.getOutputStream(); + + StringWriter sw = new StringWriter(); + XMLOutputFactory xmlof = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = xmlof.createXMLStreamWriter(sw); + + writer.writeStartDocument("utf-8", "1.0"); + writer.writeStartElement("s:Envelope"); + writer.writeAttribute("s:encodingStyle", "http://schemas.xmlsoap.org/soap/encoding/"); + writer.writeNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/"); + writer.writeStartElement("s:Body"); + writer.writeStartElement("u:Play"); + writer.writeNamespace("u", "urn:schemas-upnp-org:service:AVTransport:1"); + writer.writeStartElement("InstanceID"); + writer.writeCharacters("0"); + writer.writeEndElement(); + writer.writeStartElement("Speed"); + writer.writeCharacters("1"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + byte[] xml = sw.toString().getBytes(); + outputStream.write(xml); + outputStream.close(); + connection.getInputStream(); + } + + @Override + public void play(String url, String title, String creator, String mimeType, ItemClass itemClass) throws IOException, XMLStreamException { + HttpURLConnection connection = (HttpURLConnection) controlUrl.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "text/xml;charset=utf-8"); + connection.setRequestProperty("Soapaction", "\"urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI\""); + connection.setDoOutput(true); + OutputStream outputStream = connection.getOutputStream(); + + StringWriter didlSw = new StringWriter(); + XMLOutputFactory didlXmlof = XMLOutputFactory.newInstance(); + XMLStreamWriter didlWriter = didlXmlof.createXMLStreamWriter(didlSw); + didlWriter.writeStartElement("DIDL-Lite"); + didlWriter.writeNamespace("", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"); + didlWriter.writeNamespace("dc", "http://purl.org/dc/elements/1.1/"); + didlWriter.writeNamespace("dlna", "urn:schemas-dlna-org:metadata-1-0/"); + didlWriter.writeNamespace("pv", "http://www.pv.com/pvns/"); + didlWriter.writeNamespace("sec", "http://www.sec.co.kr/"); + didlWriter.writeNamespace("upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/"); + didlWriter.writeStartElement("item"); + didlWriter.writeAttribute("id", "/test/123"); + didlWriter.writeAttribute("parentID", "/test"); + didlWriter.writeAttribute("restricted", "1"); + didlWriter.writeStartElement("upnp:class"); + didlWriter.writeCharacters(itemClass.upnpClass); + didlWriter.writeEndElement(); + didlWriter.writeStartElement("dc:title"); + didlWriter.writeCharacters(title); + didlWriter.writeEndElement(); + didlWriter.writeStartElement("dc:creator"); + didlWriter.writeCharacters(creator); + didlWriter.writeEndElement(); + didlWriter.writeStartElement("res"); + didlWriter.writeAttribute("protocolInfo", "http-get:*:" + mimeType + ":*"); // TODO: add DLNA-specific stuff + didlWriter.writeCharacters(url); + didlWriter.writeEndElement(); + didlWriter.writeEndElement(); + didlWriter.writeEndElement(); + didlWriter.writeEndDocument(); + didlWriter.close(); + + StringWriter sw = new StringWriter(); + XMLOutputFactory xmlof = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = xmlof.createXMLStreamWriter(sw); + writer.writeStartDocument("utf-8", "1.0"); + writer.writeStartElement("s:Envelope"); + writer.writeAttribute("s:encodingStyle", "http://schemas.xmlsoap.org/soap/encoding/"); + writer.writeNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/"); + writer.writeStartElement("s:Body"); + writer.writeStartElement("u:SetAVTransportURI"); + writer.writeNamespace("u", "urn:schemas-upnp-org:service:AVTransport:1"); + writer.writeStartElement("InstanceID"); + writer.writeCharacters("0"); + writer.writeEndElement(); + writer.writeStartElement("CurrentURI"); + writer.writeCharacters(url); + writer.writeEndElement(); + writer.writeStartElement("CurrentURIMetaData"); + writer.writeCharacters(didlSw.toString()); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + byte[] xml = sw.toString().getBytes(); + outputStream.write(xml); + outputStream.close(); + connection.getInputStream(); + + play(); + } } diff --git a/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDiscoverer.java b/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDiscoverer.java index b94bb24..c103189 100644 --- a/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDiscoverer.java +++ b/src/main/java/org/schabi/newpipe/cast/protocols/upnp/UpnpDiscoverer.java @@ -19,6 +19,9 @@ import java.util.concurrent.TimeoutException; import org.schabi.newpipe.cast.Device; import org.schabi.newpipe.cast.Discoverer; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; public class UpnpDiscoverer extends Discoverer { private static final UpnpDiscoverer instance = new UpnpDiscoverer(); @@ -33,7 +36,7 @@ public class UpnpDiscoverer extends Discoverer { private class ReceiveDevices implements Callable { @Override - public Object call() throws IOException { + public Object call() throws IOException, ParserConfigurationException, SAXException { devices = new ArrayList(); while (true) { byte[] buffer = new byte[1024]; @@ -74,12 +77,12 @@ public class UpnpDiscoverer extends Discoverer { InetSocketAddress address = new InetSocketAddress(ip, 1900); socket.bind(address); - byte[] request = new String("M-SEARCH * HTTP/1.1\n" + - "HOST: 239.255.255.250:1900\n" + - "MAN: \"ssdp:discover\"\n" + - "MX: 5\n" + - "ST: urn:schemas-upnp-org:device:MediaRenderer:1\n" + - "CFPN.UPNP.ORG: PipeCast\n\n").getBytes(); + byte[] request = ("M-SEARCH * HTTP/1.1\n" + + "HOST: 239.255.255.250:1900\n" + + "MAN: \"ssdp:discover\"\n" + + "MX: 5\n" + + "ST: urn:schemas-upnp-org:device:MediaRenderer:1\n" + + "CFPN.UPNP.ORG: PipeCast\n\n").getBytes(); DatagramPacket requestDatagram = new DatagramPacket(request, request.length, Inet4Address.getByName("239.255.255.250"), 1900); socket.send(requestDatagram);