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:
@@ -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
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
12
src/main/java/org/schabi/newpipe/cast/ItemClass.java
Normal file
12
src/main/java/org/schabi/newpipe/cast/ItemClass.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user