Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add initial 'beta' UPnP MediaServer support to mythfrontend.

This still has a raft of issues and is disabled by default. To enable,
set the environment variable MYTHTV_UPNPSCANNER and switch to File
Browse Mode in MythVideo.

Known issues:-

- on at least one system here, there is some sort of race/thread lockup
that can delay mythfrontend startup for 30-40 seconds. Seems to be
something to do with the storm of UPnP related messages pinging around
following the SSDP scan - but sometimes startup is normal.

- there are various issues with StreamingRingBuffer that prevent
playback of certain file types (that should otherwise work). While some
file types still need to be filtered out (e.g. iso's), there are still
many files that should play without issue but don't. Sometimes a file
will fail first time around and then play quite happily if reselected
(mostly Play On). Other issues may be server related (e.g. MediaTomb
seems to offer up wmv files and then returns a 404).

- scanning of some servers takes too long. The Play On server, for
example, contains a sizeable file structure that takes 10s of minutes to
scan (at least 90s just for the YouTube directory)- whereas the current
timeout for scanning is 30 seconds to maintain a happy user. While the
scan results are cached, an update can be issued every couple of minutes
- which requires a rescan... All told, I think a different approach is
needed that scans and presents directories as required but I can't see
how that fits into the current MythVideo setup. Perhaps we need a more
traditional file browser for 'external' media.
  • Loading branch information...
commit 17c45ca543224efe53f2d1a119dce3d63df70eb5 1 parent fefca2c
Mark Kendall authored
View
2  mythtv/libs/libmythupnp/upnpsubscription.cpp
@@ -235,6 +235,8 @@ bool UPNPSubscription::ProcessRequest(HttpWorkerThread *pThread,
return true;
}
+ VERBOSE(VB_UPNP|VB_EXTRA, "/n/n" + body.toString(4) + "/n/n");
+
QDomNodeList properties = body.elementsByTagName("property");
QHash<QString,QString> results;
View
17 mythtv/programs/mythfrontend/mediarenderer.cpp
@@ -280,15 +280,21 @@ MediaRenderer::MediaRenderer()
// VERBOSE(VB_UPNP, QString( "MediaRenderer::Registering RenderingControl Service." ));
// m_pHttpServer->RegisterExtension( m_pUPnpRCTL= new UPnpRCTL( RootDevice() ));
- //VERBOSE(VB_UPNP, QString("MediaRenderer: Registering subscription service."));
- //UPNPSubscription *subscription =
- // new UPNPSubscription(m_pHttpServer->m_sSharePath, nPort);
- //m_pHttpServer->RegisterExtension(subscription);
+ UPNPSubscription *subscription = NULL;
+ if (getenv("MYTHTV_UPNPSCANNER"))
+ {
+ VERBOSE(VB_UPNP,
+ QString("MediaRenderer: Registering subscription service."));
+
+ subscription = new UPNPSubscription(m_pHttpServer->m_sSharePath, nPort);
+ m_pHttpServer->RegisterExtension(subscription);
+ }
Start();
// Start scanning for UPnP media servers
- //UPNPScanner::Enable(true, subscription);
+ if (subscription)
+ UPNPScanner::Enable(true, subscription);
// ensure the frontend is aware of all backends (slave and master) and
// other frontends
@@ -308,5 +314,6 @@ MediaRenderer::MediaRenderer()
MediaRenderer::~MediaRenderer()
{
+ UPNPScanner::Enable(false);
delete m_pHttpServer;
}
View
304 mythtv/programs/mythfrontend/upnpscanner.cpp
@@ -1,3 +1,4 @@
+#include <QCoreApplication>
#include <QTextCodec>
#include "mythcorecontext.h"
@@ -11,22 +12,86 @@
#define MAX_ATTEMPTS 5
#define MAX_REQUESTS 1
+QString MediaServerItem::NextUnbrowsed(void)
+{
+ // items don't need scanning
+ if (!m_url.isEmpty())
+ return QString();
+
+ // scan this container
+ if (!m_scanned)
+ {
+ m_scanned = true;
+ return m_id;
+ }
+
+ // scan children
+ QMutableMapIterator<QString,MediaServerItem> it(m_children);
+ while (it.hasNext())
+ {
+ it.next();
+ QString result = it.value().NextUnbrowsed();
+ if (!result.isEmpty())
+ return result;
+ }
+
+ return QString();
+}
+
+bool MediaServerItem::Add(MediaServerItem &item)
+{
+ if (m_id == item.m_parentid)
+ {
+ m_children.insert(item.m_id, item);
+ return true;
+ }
+
+ QMutableMapIterator<QString,MediaServerItem> it(m_children);
+ while (it.hasNext())
+ {
+ it.next();
+ if (it.value().Add(item))
+ return true;
+ }
+
+ return false;
+}
+
+void MediaServerItem::Reset(void)
+{
+ m_children.clear();
+ m_scanned = false;
+}
+
/**
* \class MediaServer
* A simple wrapper containing details about a UPnP Media Server
*/
-class MediaServer
+class MediaServer : public MediaServerItem
{
public:
MediaServer() { }
MediaServer(QUrl URL)
- : m_URL(URL), m_connectionAttempts(0), m_controlURL(QUrl()),
+ : MediaServerItem(QString("0"), QString(), QString(), QString()),
+ m_URL(URL), m_connectionAttempts(0), m_controlURL(QUrl()),
m_eventSubURL(QUrl()), m_eventSubPath(QString()),
m_friendlyName(QString("Unknown")), m_subscribed(false),
m_renewalTimerId(0), m_systemUpdateID(-1)
{
}
+ bool ResetContent(int new_id)
+ {
+ bool result = true;
+ if (m_systemUpdateID != -1)
+ {
+ result = false;
+ Reset();
+ }
+ m_systemUpdateID = new_id;
+ return result;
+ }
+
QUrl m_URL;
int m_connectionAttempts;
QUrl m_controlURL;
@@ -56,7 +121,7 @@ QMutex* UPNPScanner::gUPNPScannerLock = new QMutex(QMutex::Recursive);
UPNPScanner::UPNPScanner(UPNPSubscription *sub)
: QObject(), m_subscription(sub), m_lock(QMutex::Recursive),
m_network(NULL), m_updateTimer(NULL), m_watchdogTimer(NULL),
- m_masterHost(QString()), m_masterPort(0)
+ m_masterHost(QString()), m_masterPort(0), m_scanComplete(false)
{
}
@@ -113,17 +178,83 @@ UPNPScanner* UPNPScanner::Instance(UPNPSubscription *sub)
return gUPNPScanner;
}
+/**
+ * \fn UPNPScanner::StartFullScan
+ * Instruct the UPNPScanner thread to start a full scan of metadata from
+ * known media servers.
+ */
+void UPNPScanner::StartFullScan(void)
+{
+ MythEvent *me = new MythEvent(QString("UPNP_STARTSCAN"));
+ qApp->postEvent(this, me);
+}
/**
- * \fn UPNPScanner::ServerCount(void)
- * Returns the number of Media Servers discovered on the network.
+ * \fn UPNPScanner::GetMetadata
+ * Fill the given metadata_list and meta_dir_node with the metadata
+ * of content retrieved from known media servers.
*/
-uint UPNPScanner::ServerCount(void)
+void UPNPScanner::GetMetadata(VideoMetadataListManager::metadata_list* list,
+ meta_dir_node *node)
{
+ // nothing to see..
+ QMap<QString,QString> servers = ServerList();
+ if (servers.isEmpty())
+ return;
+
+ // Start scanning if it isn't already running
+ StartFullScan();
+
+ // wait for the scanner to complete - with a 30 second timeout
+ VERBOSE(VB_GENERAL, LOC + "Waiting for scan to complete.");
+
+ int count = 0;
+ while (!m_scanComplete && (count++ < 300))
+ usleep(100000);
+
+ // some scans may just take too long (PlayOn)
+ if (!m_scanComplete)
+ VERBOSE(VB_GENERAL, LOC + "MediaServer scan is incomplete.");
+ else
+ VERBOSE(VB_GENERAL, LOC + "MediaServer scanning finished.");
+
+
+ smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
m_lock.lock();
- uint res = m_servers.size();
+ QMutableHashIterator<QString,MediaServer*> it(m_servers);
+ while (it.hasNext())
+ {
+ it.next();
+ GetServerContent(it.value(), list, mediaservers.get());
+ }
m_lock.unlock();
- return res;
+}
+
+/**
+ * \fn UPNPScanner::GetServerContent
+ * Recursively search a MediaServerItem for video metadata and add it to
+ * the metadata_list and meta_dir_node.
+ */
+void UPNPScanner::GetServerContent(MediaServerItem *content,
+ VideoMetadataListManager::metadata_list* list,
+ meta_dir_node *node)
+{
+ if (content->m_url.isEmpty())
+ {
+ smart_dir_node container = node->addSubDir(content->m_name);
+ QMutableMapIterator<QString,MediaServerItem> it(content->m_children);
+ while (it.hasNext())
+ {
+ it.next();
+ GetServerContent(&it.value(), list, container.get());
+ }
+ return;
+ }
+
+ VideoMetadataListManager::VideoMetadataPtr item(new VideoMetadata(content->m_url));
+ item->SetTitle(content->m_name);
+ list->push_back(item);
+ node->addEntry(smart_meta_node(new meta_data_node(item.get())));
}
/**
@@ -349,7 +480,11 @@ void UPNPScanner::replyFinished(QNetworkReply *reply)
m_lock.unlock();
if (browse && valid)
+ {
ParseBrowse(url, reply);
+ // a complete scan is event driven, so trigger the next browse
+ BrowseNextContainer();
+ }
else if (description)
{
if (!valid || (valid && !ParseDescription(url, reply)))
@@ -378,7 +513,13 @@ void UPNPScanner::customEvent(QEvent *event)
// UPnP events
MythEvent *me = (MythEvent *)event;
QString ev = me->Message();
- if (ev == "UPNP_EVENT")
+
+ if (ev == "UPNP_STARTSCAN")
+ {
+ BrowseNextContainer();
+ return;
+ }
+ else if (ev == "UPNP_EVENT")
{
MythInfoMapEvent *info = (MythInfoMapEvent*)event;
if (!info)
@@ -394,10 +535,14 @@ void UPNPScanner::customEvent(QEvent *event)
m_lock.lock();
if (m_servers.contains(usn))
{
- m_servers[usn]->m_systemUpdateID = id.toInt();
- VERBOSE(VB_GENERAL, LOC + QString("New SystemUpdateID '%1' for %2")
- .arg(id).arg(usn));
- Debug();
+ int newid = id.toInt();
+ if (m_servers[usn]->m_systemUpdateID != newid)
+ {
+ m_scanComplete &= m_servers[usn]->ResetContent(newid);
+ VERBOSE(VB_GENERAL, LOC +
+ QString("New SystemUpdateID '%1' for %2").arg(id).arg(usn));
+ Debug();
+ }
}
m_lock.unlock();
return;
@@ -513,6 +658,52 @@ void UPNPScanner::Debug(void)
}
/**
+ * \fn UPNPScanner::BrowseNextContainer
+ * For each known media server, find the next container which needs to be
+ * browsed and trigger sending of the browse request (with a maximum of one
+ * active browse request for each server). Once all containers have been
+ * browsed, the scan is considered complete. N.B. failed browse requests
+ * are ignored.
+ */
+void UPNPScanner::BrowseNextContainer(void)
+{
+ QMutexLocker locker(&m_lock);
+
+ QHashIterator<QString,MediaServer*> it(m_servers);
+ bool complete = true;
+ while (it.hasNext())
+ {
+ it.next();
+ if (it.value()->m_subscribed)
+ {
+ // limit browse requests to one active per server
+ if (m_browseRequests.contains(it.value()->m_controlURL))
+ {
+ complete = false;
+ continue;
+ }
+
+ QString next = it.value()->NextUnbrowsed();
+ if (!next.isEmpty())
+ {
+ complete = false;
+ SendBrowseRequest(it.value()->m_controlURL, next);
+ continue;
+ }
+
+ VERBOSE(VB_UPNP, LOC + QString("Scan completed for %1")
+ .arg(it.value()->m_friendlyName));
+ }
+ }
+
+ if (complete)
+ {
+ VERBOSE(VB_GENERAL, LOC + QString("Media Server scan is complete."));
+ m_scanComplete = true;
+ }
+}
+
+/**
* \fn UPNPScanner::SendBrowseRequest(const QUrl&, const QString&)
* Formulates and sends a ContentDirectory Service Browse Request to the given
* control URL, requesting data for the object identified by objectid.
@@ -639,6 +830,7 @@ void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
if (data.isEmpty())
return;
+ // Open the response for parsing
QDomDocument *parent = new QDomDocument();
QString errorMessage;
int errorLine = 0;
@@ -651,8 +843,9 @@ void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
return;
}
- VERBOSE(VB_UPNP|VB_EXTRA, "\n\n" + parent->toString(4));
+ VERBOSE(VB_UPNP|VB_EXTRA, "\n\n" + parent->toString(4) + "\n\n");
+ // pull out the actual result
QDomDocument *result = NULL;
uint num = 0;
uint total = 0;
@@ -664,24 +857,63 @@ void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
delete parent;
if (!result || num < 1 || total < 1)
+ {
+ VERBOSE(VB_IMPORTANT, LOC + QString("Failed to find result for %1")
+ .arg(url.toString()));
return;
+ }
+
+ // determine the 'server' which requested the browse
+ m_lock.lock();
+ MediaServer* server = NULL;
+ QHashIterator<QString,MediaServer*> it(m_servers);
+ while (it.hasNext())
+ {
+ it.next();
+ if (url == it.value()->m_controlURL)
+ {
+ server = it.value();
+ break;
+ }
+ }
+
+ // discard unmatched responses
+ if (!server)
+ {
+ m_lock.unlock();
+ VERBOSE(VB_IMPORTANT, LOC +
+ QString("Received unknown response for %1").arg(url.toString()));
+ return;
+ }
+
+ // check the update ID
+ if (server->m_systemUpdateID != (int)updateid)
+ {
+ // if this is not the root container, this browse will now fail
+ // as the appropriate parentID will not be found
+ VERBOSE(VB_IMPORTANT, LOC +
+ QString("%1 updateID changed during browse (old %2 new %3)")
+ .arg(server->m_friendlyName).arg(server->m_systemUpdateID)
+ .arg(updateid));
+ m_scanComplete &= server->ResetContent(updateid);
+ Debug();
+ }
+
+ // find containers (directories) and actual items and add them
docElem = result->documentElement();
n = docElem.firstChild();
- QList<QStringList> items;
while (!n.isNull())
{
- FindItems(n, items);
+ FindItems(n, *server);
n = n.nextSibling();
}
-
- //foreach (QStringList list, items)
- // VERBOSE(VB_IMPORTANT, LOC + QString("%1 %2 %3 %4")
- // .arg(list[0]).arg(list[1]).arg(list[2]).arg(list[3]));
delete result;
+
+ m_lock.unlock();
}
-void UPNPScanner::FindItems(const QDomNode &n, QList<QStringList> &items)
+void UPNPScanner::FindItems(const QDomNode &n, MediaServerItem &content)
{
QDomElement node = n.toElement();
if (node.isNull())
@@ -698,12 +930,12 @@ void UPNPScanner::FindItems(const QDomNode &n, QList<QStringList> &items)
title = container.text();
next = next.nextSibling();
}
- QStringList list;
- list.append(node.attribute("id", "ERROR"));
- list.append(node.attribute("parentID", "ERROR"));
- list.append(title);
- list.append(QString()); //url
- items.append(list);
+
+ MediaServerItem container =
+ MediaServerItem(node.attribute("id", "ERROR"),
+ node.attribute("parentID", "ERROR"),
+ title, QString());
+ content.Add(container);
return;
}
@@ -724,19 +956,19 @@ void UPNPScanner::FindItems(const QDomNode &n, QList<QStringList> &items)
}
next = next.nextSibling();
}
- QStringList list;
- list.append(node.attribute("id", "ERROR"));
- list.append(node.attribute("parentID", "ERROR"));
- list.append(title);
- list.append(url);
- items.append(list);
+
+ MediaServerItem item =
+ MediaServerItem(node.attribute("id", "ERROR"),
+ node.attribute("parentID", "ERROR"),
+ title, url);
+ content.Add(item);
return;
}
QDomNode next = node.firstChild();
while (!next.isNull())
{
- FindItems(next, items);
+ FindItems(next, content);
next = next.nextSibling();
}
}
@@ -882,6 +1114,7 @@ bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
it.value()->m_eventSubURL = qeventurl;
it.value()->m_eventSubPath = eventURL;
it.value()->m_friendlyName = friendlyName;
+ it.value()->m_name = friendlyName;
break;
}
}
@@ -898,6 +1131,9 @@ bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
VERBOSE(VB_GENERAL, LOC + QString("Subscribed for %1 seconds to %2")
.arg(timeout).arg(usn));
ScheduleRenewal(usn, timeout);
+ // we only scan servers we are subscribed to - and the scan is now
+ // incomplete
+ m_scanComplete = false;
}
Debug();
View
36 mythtv/programs/mythfrontend/upnpscanner.h
@@ -12,8 +12,30 @@
#include "upnpexp.h"
#include "upnpsubscription.h"
+#include "videometadatalistmanager.h"
+
class MediaServer;
class UPNPSubscription;
+class meta_dir_node;
+
+class MediaServerItem
+{
+ public:
+ MediaServerItem() { }
+ MediaServerItem(QString id, QString parent, QString name, QString url)
+ : m_id(id), m_parentid(parent), m_name(name), m_url(url),
+ m_scanned(false) { }
+ QString NextUnbrowsed(void);
+ bool Add(MediaServerItem &item);
+ void Reset(void);
+
+ QString m_id;
+ QString m_parentid;
+ QString m_name;
+ QString m_url;
+ bool m_scanned;
+ QMap<QString, MediaServerItem> m_children;
+};
class UPNPScanner : public QObject
{
@@ -25,7 +47,9 @@ class UPNPScanner : public QObject
static void Enable(bool enable, UPNPSubscription *sub = NULL);
static UPNPScanner* Instance(UPNPSubscription *sub = NULL);
- uint ServerCount(void);
+ void StartFullScan(void);
+ void GetMetadata(VideoMetadataListManager::metadata_list* list,
+ meta_dir_node *node);
QMap<QString,QString> ServerList(void);
protected:
@@ -44,6 +68,7 @@ class UPNPScanner : public QObject
void ScheduleUpdate(void);
void CheckFailure(const QUrl &url);
void Debug(void);
+ void BrowseNextContainer(void);
void SendBrowseRequest(const QUrl &url, const QString &objectid);
void AddServer(const QString &usn, const QString &url);
void RemoveServer(const QString &usn);
@@ -51,7 +76,7 @@ class UPNPScanner : public QObject
// xml parsing of browse requests
void ParseBrowse(const QUrl &url, QNetworkReply *reply);
- void FindItems(const QDomNode &n, QList<QStringList> &items);
+ void FindItems(const QDomNode &n, MediaServerItem &content);
QDomDocument* FindResult(const QDomNode &n, uint &num,
uint &total, uint &updateid);
@@ -64,6 +89,11 @@ class UPNPScanner : public QObject
void ParseService(QDomElement &element, QString &controlURL,
QString &eventURL);
+ // convert MediaServerItems to video metadata
+ void GetServerContent(MediaServerItem *content,
+ VideoMetadataListManager::metadata_list* list,
+ meta_dir_node *node);
+
private:
static UPNPScanner* gUPNPScanner;
static bool gUPNPScannerEnabled;
@@ -84,6 +114,8 @@ class UPNPScanner : public QObject
QString m_masterHost;
int m_masterPort;
+
+ bool m_scanComplete;
};
#endif // UPNPSCANNER_H
View
10 mythtv/programs/mythfrontend/videolist.cpp
@@ -21,6 +21,8 @@
#include "videolist.h"
#include "videodlg.h"
+#include "upnpscanner.h"
+
class TreeNodeDataPrivate
{
public:
@@ -1050,6 +1052,10 @@ void VideoListImp::buildFsysList()
// Fill metadata from directory structure
//
+ // if available, start a UPnP MediaServer update first
+ if (UPNPScanner::Instance())
+ UPNPScanner::Instance()->StartFullScan();
+
typedef std::vector<std::pair<QString, QString> > node_to_path_list;
node_to_path_list node_paths;
@@ -1115,6 +1121,10 @@ void VideoListImp::buildFsysList()
buildFileList(root, ml, p->second);
}
+ // retrieve any MediaServer data that may be available
+ if (UPNPScanner::Instance())
+ UPNPScanner::Instance()->GetMetadata(&ml, &m_metadata_tree);
+
// See if we can find this filename in DB
if (m_LoadMetaData)
{
Please sign in to comment.
Something went wrong with that request. Please try again.