1
0
mirror of https://github.com/TeamNewPipe/PipeCast synced 2025-10-06 00:12:51 +02:00

Add UPnP A/V playback

This commit is contained in:
wb9688
2020-02-09 11:32:56 +01:00
parent 23e6b6156b
commit c3da94e31a
5 changed files with 181 additions and 32 deletions

View File

@@ -1,12 +1,12 @@
# PipeCast # 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 ## Supported protocols
The following protocols are currently supported by PipeCast: The following protocols are currently supported by PipeCast:
- UPnP - UPnP A/V
## License ## License

View File

@@ -1,11 +1,17 @@
package org.schabi.newpipe.cast; package org.schabi.newpipe.cast;
import java.io.IOException;
import javax.xml.stream.XMLStreamException;
public abstract class Device { public abstract class Device {
protected final String location; public final String location;
public Device(String location) { public Device(String location) {
this.location = location; this.location = location;
} }
public abstract String getName(); public abstract String getName();
public abstract void play(String url, String title, String creator, String mimeType, ItemClass itemClass) throws IOException, XMLStreamException;
} }

View File

@@ -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;
}
}

View File

@@ -3,56 +3,184 @@ package org.schabi.newpipe.cast.protocols.upnp;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader; import java.io.StringReader;
import java.io.StringWriter;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; 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.Device;
import org.schabi.newpipe.cast.ItemClass;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
public class UpnpDevice extends Device { public class UpnpDevice extends Device {
private Document description; private Document description;
private Element device; private Element device;
private URL controlUrl;
public UpnpDevice(String location) throws IOException { public UpnpDevice(String location) throws IOException, ParserConfigurationException, SAXException {
super(location); super(location);
getDescription(); getDescription();
} }
private void getDescription() throws IOException { private void getDescription() throws IOException, ParserConfigurationException, SAXException {
try { URL url = new URL(location);
URL url = new URL(location); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET");
connection.setRequestMethod("GET"); BufferedReader input = new BufferedReader(new InputStreamReader(connection.getInputStream()));
BufferedReader input = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line;
String line; StringBuilder response = new StringBuilder();
StringBuilder response = new StringBuilder(); while ((line = input.readLine()) != null) {
while ((line = input.readLine()) != null) { response.append(line);
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);
} }
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 @Override
public String getName() { public String getName() {
return device.getElementsByTagName("friendlyName").item(0).getTextContent(); 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();
}
} }

View File

@@ -19,6 +19,9 @@ import java.util.concurrent.TimeoutException;
import org.schabi.newpipe.cast.Device; import org.schabi.newpipe.cast.Device;
import org.schabi.newpipe.cast.Discoverer; import org.schabi.newpipe.cast.Discoverer;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
public class UpnpDiscoverer extends Discoverer { public class UpnpDiscoverer extends Discoverer {
private static final UpnpDiscoverer instance = new UpnpDiscoverer(); private static final UpnpDiscoverer instance = new UpnpDiscoverer();
@@ -33,7 +36,7 @@ public class UpnpDiscoverer extends Discoverer {
private class ReceiveDevices implements Callable<Object> { private class ReceiveDevices implements Callable<Object> {
@Override @Override
public Object call() throws IOException { public Object call() throws IOException, ParserConfigurationException, SAXException {
devices = new ArrayList<Device>(); devices = new ArrayList<Device>();
while (true) { while (true) {
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
@@ -74,12 +77,12 @@ public class UpnpDiscoverer extends Discoverer {
InetSocketAddress address = new InetSocketAddress(ip, 1900); InetSocketAddress address = new InetSocketAddress(ip, 1900);
socket.bind(address); socket.bind(address);
byte[] request = new String("M-SEARCH * HTTP/1.1\n" + byte[] request = ("M-SEARCH * HTTP/1.1\n" +
"HOST: 239.255.255.250:1900\n" + "HOST: 239.255.255.250:1900\n" +
"MAN: \"ssdp:discover\"\n" + "MAN: \"ssdp:discover\"\n" +
"MX: 5\n" + "MX: 5\n" +
"ST: urn:schemas-upnp-org:device:MediaRenderer:1\n" + "ST: urn:schemas-upnp-org:device:MediaRenderer:1\n" +
"CFPN.UPNP.ORG: PipeCast\n\n").getBytes(); "CFPN.UPNP.ORG: PipeCast\n\n").getBytes();
DatagramPacket requestDatagram = new DatagramPacket(request, request.length, Inet4Address.getByName("239.255.255.250"), 1900); DatagramPacket requestDatagram = new DatagramPacket(request, request.length, Inet4Address.getByName("239.255.255.250"), 1900);
socket.send(requestDatagram); socket.send(requestDatagram);