1
0
mirror of https://github.com/TeamNewPipe/PipeCast synced 2025-10-05 16:02:48 +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 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

View File

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

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.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();
}
}

View File

@@ -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<Object> {
@Override
public Object call() throws IOException {
public Object call() throws IOException, ParserConfigurationException, SAXException {
devices = new ArrayList<Device>();
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);