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:
@@ -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
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
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.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();
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user