Browse Source

Working harmony control via Roku ECP

Tobias Hieta 9 years ago
parent
commit
88a87aa5c8

+ 5 - 0
resources/settings/settings_description.json

@@ -79,6 +79,11 @@
         // Warning: the default must be the same as the one in preinitQt().
         "default": false,
         "platforms": [ "windows" ]
+      },
+      {
+        "value": "clientUUID",
+        "default": "",
+        "hidden": false
       }
     ]
   },

+ 128 - 1
src/input/InputRoku.cpp

@@ -9,10 +9,13 @@
 #include "qhttpserverrequest.hpp"
 
 #include <QUrl>
+#include <QUdpSocket>
 #include <QXmlStreamWriter>
 
 using namespace qhttp::server;
 
+#define ROKU_SERIAL_NUMBER "12345678900"
+
 /////////////////////////////////////////////////////////////////////////////////////////
 bool InputRoku::initInput()
 {
@@ -26,14 +29,93 @@ bool InputRoku::initInput()
 
   connect(m_server, &QHttpServer::newRequest, this, &InputRoku::handleRequest);
 
+  m_ssdpSocket = new QUdpSocket(this);
+  if (!m_ssdpSocket->bind(QHostAddress::AnyIPv4, 1900, QUdpSocket::ReuseAddressHint | QUdpSocket::ShareAddress))
+  {
+    QLOG_WARN() << "Failed to bind to SSDP socket";
+    return false;
+  }
+
+  QHostAddress multicast("239.255.255.250");
+  m_ssdpSocket->joinMulticastGroup(multicast);
+
+  connect(m_ssdpSocket, &QUdpSocket::readyRead, this, &InputRoku::ssdpRead);
+
   return true;
 }
 
+/////////////////////////////////////////////////////////////////////////////////////////
+void InputRoku::ssdpRead()
+{
+  while (m_ssdpSocket->hasPendingDatagrams())
+  {
+    QLOG_DEBUG() << "Got SSDP datagram";
+
+    QByteArray datagram;
+    datagram.resize((int)m_ssdpSocket->pendingDatagramSize());
+
+    QHostAddress sender;
+    quint16 senderPort;
+
+    m_ssdpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
+
+    QLOG_DEBUG() << "datagram:" << QString::fromUtf8(datagram);
+    parseSSDPData(datagram, sender, senderPort);
+  }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+void InputRoku::parseSSDPData(const QByteArray& data, const QHostAddress& sender, quint16 port)
+{
+  if (data.contains("M-SEARCH * HTTP/1.1"))
+    m_ssdpSocket->writeDatagram(getSSDPPacket(), sender, port);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+QByteArray InputRoku::getSSDPPacket()
+{
+
+  /*
+    HTTP/1.1 200 OK
+    Cache-Control: max-age=300
+    ST: roku:ecp
+    Location: http://192.168.1.134:8060/
+    USN: uuid:roku:ecp:P0A070000007
+   */
+
+  QByteArray packetData;
+
+  // Header
+  packetData.append("HTTP/1.1 200 OK\r\n");
+  packetData.append("Cache-Control: max-age=300\r\n");
+  packetData.append("ST: roku:ecp\r\n");
+  packetData.append("Ext: \r\n");
+  packetData.append("Server: Roku UPnP/1.0 MiniUPnPd/1.4\r\n");
+
+  packetData.append("Location: http://");
+  packetData.append(Utils::PrimaryIPv4Address());
+  packetData.append(":8060/\r\n");
+
+  packetData.append("USN: uuid:roku:ecp:");
+  packetData.append(ROKU_SERIAL_NUMBER);
+  packetData.append("\r\n\r\n");
+
+  QLOG_DEBUG() << "Reply: " << QString::fromUtf8(packetData);
+
+  return packetData;
+}
 /////////////////////////////////////////////////////////////////////////////////////////
 void InputRoku::handleRequest(QHttpRequest* request, QHttpResponse* response)
 {
   QString path = request->url().path();
-  if (path == "/query/apps")
+
+  QLOG_DEBUG() << "Request:" << path;
+
+  if (path == "/")
+  {
+    handleRootInfo(request, response);
+  }
+  else if (path == "/query/apps")
   {
     handleQueryApps(request, response);
   }
@@ -154,3 +236,48 @@ void InputRoku::handleKeyPress(QHttpRequest* request, QHttpResponse* response)
   response->end();
   return;
 }
+
+/////////////////////////////////////////////////////////////////////////////////////////
+void InputRoku::handleRootInfo(QHttpRequest* request, QHttpResponse* response)
+{
+  QByteArray data;
+  QXmlStreamWriter writer(&data);
+  writer.setAutoFormatting(true);
+
+  writer.writeStartDocument();
+  writer.writeStartElement("urn:schemas-upnp-org:device-1-0", "root");
+
+  writer.writeStartElement("specVersion");
+  writer.writeTextElement("major", "1");
+  writer.writeTextElement("minor", "0");
+  writer.writeEndElement(); // specVersion
+
+  writer.writeStartElement("device");
+  writer.writeTextElement("deviceType", "urn:roku-com:device:player:1-0");
+  writer.writeTextElement("friendlyName", Utils::ComputerName());
+  writer.writeTextElement("manufacturer", "Roku");
+  writer.writeTextElement("manufacturerURL", "http://www.roku.com");
+  writer.writeTextElement("modelDescription", "Roku Streaming Player Network Media");
+  writer.writeTextElement("modelName", "Roku 3");
+  writer.writeTextElement("modelNumber", "4200X");
+  writer.writeTextElement("modelURL", "http://www.roku.com");
+  writer.writeTextElement("serialNumber", ROKU_SERIAL_NUMBER);
+  writer.writeTextElement("UDN", "uuid:" + Utils::ClientUUID());
+
+  writer.writeStartElement("serviceList");
+  writer.writeStartElement("service");
+  writer.writeTextElement("serviceType", "urn:roku-com:service:ecp:1");
+  writer.writeTextElement("serviceId", "urn:roku-com:serviceId:ecp1-0");
+  writer.writeTextElement("controlURL", "");
+  writer.writeTextElement("eventSubURL", "");
+  writer.writeTextElement("SCPDURL", "ecp_SCPD.xml");
+  writer.writeEndElement(); // service
+  writer.writeEndElement(); // serviceList
+
+  writer.writeEndElement(); // root
+  writer.writeEndDocument();
+
+  response->setStatusCode(qhttp::ESTATUS_OK);
+  response->write(data);
+  response->end();
+}

+ 7 - 0
src/input/InputRoku.h

@@ -7,6 +7,7 @@
 
 #include "input/InputComponent.h"
 #include "qhttpserver.hpp"
+#include <QUdpSocket>
 
 class InputRoku : public InputBase
 {
@@ -24,6 +25,12 @@ private:
   void handleQueryDeviceInfo(qhttp::server::QHttpRequest* request, qhttp::server::QHttpResponse* response);
 
   qhttp::server::QHttpServer* m_server;
+  QUdpSocket* m_ssdpSocket;
+
+  void ssdpRead();
+  void parseSSDPData(const QByteArray& data, const QHostAddress& sender, quint16 port);
+  QByteArray getSSDPPacket();
+  void handleRootInfo(qhttp::server::QHttpRequest* request, qhttp::server::QHttpResponse* response);
 };
 
 #endif //PLEXMEDIAPLAYER_ROKUREMOTECOMPONENT_H

+ 38 - 0
src/utils/Utils.cpp

@@ -9,6 +9,8 @@
 #include <QHostInfo>
 #include <QJsonDocument>
 #include <QVariant>
+#include <qnetworkinterface.h>
+#include <QUuid>
 
 #include "settings/SettingsComponent.h"
 #include "settings/SettingsSection.h"
@@ -96,3 +98,39 @@ QString Utils::CurrentUserId()
 
   return QString();
 }
+
+/////////////////////////////////////////////////////////////////////////////////////////
+QString Utils::PrimaryIPv4Address()
+{
+  QList<QNetworkInterface> ifs = QNetworkInterface::allInterfaces();
+  foreach(const QNetworkInterface& iface, ifs)
+  {
+    if (iface.isValid() && iface.flags() & QNetworkInterface::IsUp)
+    {
+      QList<QHostAddress> addresses = iface.allAddresses();
+      foreach(const QHostAddress& addr, addresses)
+      {
+        if (!addr.isLoopback() && !addr.isMulticast() && addr.protocol() == QAbstractSocket::IPv4Protocol)
+        {
+          QLOG_DEBUG() << "I think that" << addr.toString() << "is my primary address";
+          return addr.toString();
+        }
+      }
+    }
+  }
+  return "";
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+QString Utils::ClientUUID()
+{
+  QString storedUUID = SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "clientUUID").toString();
+  if (storedUUID.isEmpty())
+  {
+    QString newUUID = QUuid::createUuid().toString();
+    newUUID = newUUID.replace("{", "").replace("}", "");
+    SettingsComponent::Get().setValue(SETTINGS_SECTION_MAIN, "clientUUID", newUUID);
+    return newUUID;
+  }
+  return storedUUID;
+}

+ 2 - 0
src/utils/Utils.h

@@ -52,6 +52,8 @@ namespace Utils
   QJsonDocument OpenJsonDocument(const QString& path, QJsonParseError* err);
   QString CurrentUserId();
   QString ComputerName();
+  QString PrimaryIPv4Address();
+  QString ClientUUID();
 }
 
 #endif // UTILS_H