MythTV master
vboxutils.cpp
Go to the documentation of this file.
1#include <chrono> // for milliseconds
2#include <thread> // for sleep_for
3
4// Qt
5#include <QString>
6#include <QStringList>
7#include <QDomDocument>
8#include <QRegularExpression>
9
10// MythTV headers
14#include "libmythupnp/ssdp.h"
16#include "vboxutils.h"
17
18#define LOC QString("VBox: ")
19
20static constexpr const char* QUERY_BOARDINFO
21{ "http://{URL}/cgi-bin/HttpControl/HttpControlApp?OPTION=1&Method=QueryBoardInfo" };
22static constexpr const char* QUERY_CHANNELS
23{ "http://{URL}/cgi-bin/HttpControl/HttpControlApp?OPTION=1&Method=GetXmltvChannelsList" \
24 "&FromChIndex=FirstChannel&ToChIndex=LastChannel&FilterBy=All" };
25
26static constexpr std::chrono::milliseconds SEARCH_TIME { 3s };
27static constexpr const char* VBOX_URI { "urn:schemas-upnp-org:device:MediaServer:1" };
28static constexpr const char* VBOX_UDN { "uuid:b7531642-0123-3210" };
29
30// static method
31QStringList VBox::probeDevices(void)
32{
33 const std::chrono::milliseconds milliSeconds { SEARCH_TIME };
34 auto seconds = duration_cast<std::chrono::seconds>(milliSeconds);
35
36 // see if we have already found one or more vboxes
37 QStringList result = VBox::doUPNPSearch();
38
39 if (!result.isEmpty())
40 return result;
41
42 // non found so start a new search
43 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Using UPNP to search for Vboxes (%1 secs)")
44 .arg(seconds.count()));
45
47
48 // Search for a total of 'milliSeconds' ms, sending new search packet
49 // about every 250 ms until less than one second remains.
50 MythTimer totalTime; totalTime.start();
51 MythTimer searchTime; searchTime.start();
52 while (totalTime.elapsed() < milliSeconds)
53 {
54 std::this_thread::sleep_for(25ms);
55 auto ttl = duration_cast<std::chrono::seconds>(milliSeconds - totalTime.elapsed());
56 if ((searchTime.elapsed() > 249ms) && (ttl > 1s))
57 {
58 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("UPNP Search %1 secs")
59 .arg(ttl.count()));
61 searchTime.start();
62 }
63 }
64
65 return VBox::doUPNPSearch();
66}
67
68QStringList VBox::doUPNPSearch(void)
69{
70 QStringList result;
71
73
74 if (!vboxes)
75 {
76 LOG(VB_GENERAL, LOG_DEBUG, LOC + "No UPnP VBoxes found");
77 return {};
78 }
79
80 int count = vboxes->Count();
81 if (count)
82 {
83 LOG(VB_GENERAL, LOG_DEBUG, LOC +
84 QString("Found %1 possible VBoxes").arg(count));
85 }
86 else
87 {
88 LOG(VB_GENERAL, LOG_ERR, LOC +
89 "No UPnP VBoxes found, but SSDPCache::Instance()->Find() not NULL");
90 }
91
92 EntryMap map;
93 vboxes->GetEntryMap(map);
94
95 for (auto *BE : std::as_const(map))
96 {
97 if (!BE->GetDeviceDesc())
98 {
99 LOG(VB_GENERAL, LOG_INFO, LOC + QString("GetDeviceDesc() failed for %1").arg(BE->GetFriendlyName()));
100 continue;
101 }
102
103 QString friendlyName = BE->GetDeviceDesc()->m_rootDevice.m_sFriendlyName;
104 QString ip = BE->GetDeviceDesc()->m_hostUrl.host();
105 QString udn = BE->GetDeviceDesc()->m_rootDevice.m_sUDN;
106 int port = BE->GetDeviceDesc()->m_hostUrl.port();
107
108 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Found possible VBox at %1 (%2:%3)")
109 .arg(friendlyName, ip, QString::number(port)));
110
111 if (udn.startsWith(VBOX_UDN))
112 {
113 // we found one
114 QString id;
115 int startPos = friendlyName.indexOf('(');
116 int endPos = friendlyName.indexOf(')');
117
118 if (startPos != -1 && endPos != -1)
119 id = friendlyName.mid(startPos + 1, endPos - startPos - 1);
120 else
121 id = friendlyName;
122
123 // get a list of tuners on this VBOX
124 QStringList tuners;
125
126 VBox *vbox = new VBox(ip);
127 tuners = vbox->getTuners();
128 delete vbox;
129
130 for (int x = 0; x < tuners.count(); x++)
131 {
132 // add a device in the format ID IP TUNERNO TUNERTYPE
133 // eg vbox_3718 192.168.1.204 1 DVBT/T2
134 const QString& tuner = tuners.at(x);
135 QString device = QString("%1 %2 %3").arg(id, ip, tuner);
136 result << device;
137 LOG(VB_GENERAL, LOG_INFO, QString("Found VBox - %1").arg(device));
138 }
139 }
140
141 BE->DecrRef();
142 }
143
144 vboxes->DecrRef();
145 vboxes = nullptr;
146
147 return result;
148}
149
150// static method
151QString VBox::getIPFromVideoDevice(const QString& dev)
152{
153 // dev is of the form xx.xx.xx.xx-n-t or xxxxxxx-n-t
154 // where xx is either an ip address or vbox id
155 // n is the tuner number and t is the tuner type ie DVBT/T2
156 QStringList devItems = dev.split("-");
157
158 if (devItems.size() != 3)
159 {
160 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Got malformed videodev %1").arg(dev));
161 return {};
162 }
163
164 QString id = devItems.at(0).trimmed();
165
166 // if we already have an ip address use that
167 static const QRegularExpression ipRE { R"(^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$)" };
168 auto match = ipRE.match(id);
169 if (match.hasMatch())
170 return id;
171
172 // we must have a vbox id so look it up to find the ip address
173 QStringList vboxes = VBox::probeDevices();
174
175 for (int x = 0; x < vboxes.count(); x++)
176 {
177 QStringList vboxItems = vboxes.at(x).split(" ");
178 if (vboxItems.size() != 4)
179 {
180 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Got malformed probed device %1").arg(vboxes.at(x)));
181 continue;
182 }
183
184 const QString& vboxID = vboxItems.at(0);
185 QString vboxIP = vboxItems.at(1);
186
187 if (vboxID == id)
188 return vboxIP;
189 }
190
191 // if we get here we didn't find it
192 return {};
193}
194
195QDomDocument *VBox::getBoardInfo(void)
196{
197 auto *xmlDoc = new QDomDocument();
198 QString query = QUERY_BOARDINFO;
199
200 query.replace("{URL}", m_url);
201
202 if (!sendQuery(query, xmlDoc))
203 {
204 delete xmlDoc;
205 return nullptr;
206 }
207
208 return xmlDoc;
209}
210
212{
213 // assume if we can download the board info we have a good connection
214 return (getBoardInfo() != nullptr);
215}
216
218{
219 QString requiredVersion = VBOX_MIN_API_VERSION;
220 QStringList sList = requiredVersion.split('.');
221
222 // sanity check this looks like a VBox version string
223 if (sList.count() < 3 || !requiredVersion.startsWith("V"))
224 {
225 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Failed to parse required version from %1").arg(requiredVersion));
226 version = "UNKNOWN";
227 return false;
228 }
229
230 int requiredMajor = sList[1].toInt();
231 int requiredMinor = sList[2].toInt();
232
233 int major = 0;
234 int minor = 0;
235
236 QDomDocument *xmlDoc = getBoardInfo();
237 QDomElement elem = xmlDoc->documentElement();
238
239 if (!elem.isNull())
240 {
241 version = getStrValue(elem, "SoftwareVersion");
242
243 sList = version.split('.');
244
245 // sanity check this looks like a VBox version string
246 if (sList.count() < 3 || !(version.startsWith("VB.") || version.startsWith("VJ.")
247 || version.startsWith("VT.")))
248 {
249 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Failed to parse version from %1").arg(version));
250 delete xmlDoc;
251 return false;
252 }
253
254 major = sList[1].toInt();
255 minor = sList[2].toInt();
256 }
257
258 delete xmlDoc;
259
260 LOG(VB_GENERAL, LOG_INFO, LOC + QString("CheckVersion - required: %1, actual: %2")
262
263 if (major < requiredMajor)
264 return false;
265
266 if (major == requiredMajor && minor < requiredMinor)
267 return false;
268
269 return true;
270}
271
273QStringList VBox::getTuners(void)
274{
275 QStringList result;
276
277 QDomDocument *xmlDoc = getBoardInfo();
278 QDomElement elem = xmlDoc->documentElement();
279
280 if (!elem.isNull())
281 {
282 int noTuners = getIntValue(elem, "TunersNumber");
283
284 for (int x = 1; x <= noTuners; x++)
285 {
286 QString tuner = getStrValue(elem, QString("Tuner%1").arg(x));
287 QString s = QString("%1 %2").arg(x).arg(tuner);
288 result.append(s);
289 }
290 }
291
292 delete xmlDoc;
293
294 return result;
295}
296
297
299{
300 auto *result = new vbox_chan_map_t;
301 auto *xmlDoc = new QDomDocument();
302 QString query = QUERY_CHANNELS;
303
304 query.replace("{URL}", m_url);
305
306 if (!sendQuery(query, xmlDoc))
307 {
308 delete xmlDoc;
309 delete result;
310 return nullptr;
311 }
312
313 QDomNodeList chanNodes = xmlDoc->elementsByTagName("channel");
314
315 for (int x = 0; x < chanNodes.count(); x++)
316 {
317 QDomElement chanElem = chanNodes.at(x).toElement();
318 QString xmltvid = chanElem.attribute("id", "UNKNOWN_ID");
319 QString name = getStrValue(chanElem, "display-name", 0);
320 QString chanType = getStrValue(chanElem, "display-name", 1);
321 QString triplet = getStrValue(chanElem, "display-name", 2);
322 bool fta = (getStrValue(chanElem, "display-name", 3) == "Free");
323 QString lcn = getStrValue(chanElem, "display-name", 4);
324 uint serviceID = triplet.right(4).toUInt(nullptr, 16);
325
326 QString transType = "UNKNOWN";
327 QStringList slist = triplet.split('-');
328 uint networkID = slist[2].left(4).toUInt(nullptr, 16);
329 uint transportID = slist[2].mid(4, 4).toUInt(nullptr, 16);
330 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("NIT/TID/SID %1 %2 %3)").arg(networkID).arg(transportID).arg(serviceID));
331
332 //sanity check - the triplet should look something like this: T-GER-111100020001
333 // where T is the tuner type, GER is the country, and the numbers are the NIT/TID/SID
334 if (slist.count() == 3)
335 transType = slist[0];
336
337 QString icon = "";
338 QDomNodeList iconNodes = chanElem.elementsByTagName("icon");
339 if (iconNodes.count())
340 {
341 QDomElement iconElem = iconNodes.at(0).toElement();
342 icon = iconElem.attribute("src", "");
343 }
344
345 QString url = "";
346 QDomNodeList urlNodes = chanElem.elementsByTagName("url");
347 if (urlNodes.count())
348 {
349 QDomElement urlElem = urlNodes.at(0).toElement();
350 url = urlElem.attribute("src", "");
351 }
352
353 VBoxChannelInfo chanInfo(name, xmltvid, url, fta, chanType, transType, serviceID, networkID, transportID);
354 result->insert(lcn, chanInfo);
355 }
356
357 return result;
358}
359
360bool VBox::sendQuery(const QString& query, QDomDocument* xmlDoc)
361{
362 QByteArray result;
363
364 if (!GetMythDownloadManager()->download(query, &result, true))
365 return false;
366
367#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
368 QString errorMsg;
369 int errorLine = 0;
370 int errorColumn = 0;
371
372 if (!xmlDoc->setContent(result, false, &errorMsg, &errorLine, &errorColumn))
373 {
374 LOG(VB_GENERAL, LOG_ERR, LOC +
375 QString("Error parsing: %1\nat line: %2 column: %3 msg: %4").
376 arg(query).arg(errorLine).arg(errorColumn).arg(errorMsg));
377 return false;
378 }
379#else
380 auto parseResult = xmlDoc->setContent(result);
381 if (!parseResult)
382 {
383 LOG(VB_GENERAL, LOG_ERR, LOC +
384 QString("Error parsing: %1\nat line: %2 column: %3 msg: %4")
385 .arg(query).arg(parseResult.errorLine)
386 .arg(parseResult.errorColumn).arg(parseResult.errorMessage));
387 return false;
388 }
389#endif
390
391 // check for a status or error element
392 QDomNodeList statusNodes = xmlDoc->elementsByTagName("Status");
393
394 if (!statusNodes.count())
395 statusNodes = xmlDoc->elementsByTagName("Error");
396
397 if (statusNodes.count())
398 {
399 QDomElement elem = statusNodes.at(0).toElement();
400 if (!elem.isNull())
401 {
402 ErrorCode errorCode = (ErrorCode)getIntValue(elem, "ErrorCode");
403 QString errorDesc = getStrValue(elem, "ErrorDescription");
404
405 if (errorCode == SUCCESS)
406 return true;
407
408 LOG(VB_GENERAL, LOG_ERR, LOC +
409 QString("API Error: %1 - %2, Query was: %3").arg(errorCode).arg(errorDesc, query));
410
411 return false;
412 }
413 }
414
415 // no error detected so assume we got a valid xml result
416 return true;
417}
418
419QString VBox::getStrValue(const QDomElement &element, const QString &name, int index)
420{
421 QDomNodeList nodes = element.elementsByTagName(name);
422 if (!nodes.isEmpty())
423 {
424 if (index >= nodes.count())
425 index = 0;
426 QDomElement e = nodes.at(index).toElement();
427 return getFirstText(e);
428 }
429
430 return {};
431}
432
433int VBox::getIntValue(const QDomElement &element, const QString &name, int index)
434{
435 QString value = getStrValue(element, name, index);
436
437 return value.toInt();
438}
439
440QString VBox::getFirstText(QDomElement &element)
441{
442 for (QDomNode dname = element.firstChild(); !dname.isNull();
443 dname = dname.nextSibling())
444 {
445 QDomText t = dname.toText();
446 if (!t.isNull())
447 return t.data();
448 }
449 return {};
450}
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:14
std::chrono::milliseconds elapsed(void)
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:91
void start(void)
starts measuring elapsed time.
Definition: mythtimer.cpp:47
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
uint Count(void) const
Definition: ssdpcache.h:50
void GetEntryMap(EntryMap &map)
Returns a copy of the EntryMap.
Definition: ssdpcache.cpp:87
static SSDPCache * Instance()
Definition: ssdpcache.cpp:285
SSDPCacheEntries * Find(const QString &sURI)
Finds the SSDPCacheEntries in the cache, returns nullptr when absent.
Definition: ssdpcache.cpp:341
static SSDP * Instance()
Definition: ssdp.cpp:57
void PerformSearch(const QString &sST, std::chrono::seconds timeout=2s)
Send a SSDP discover multicast datagram.
Definition: ssdp.cpp:154
Definition: vboxutils.h:17
bool checkVersion(QString &version)
Definition: vboxutils.cpp:217
ErrorCode
Definition: vboxutils.h:36
@ SUCCESS
Definition: vboxutils.h:37
static int getIntValue(const QDomElement &element, const QString &name, int index=0)
Definition: vboxutils.cpp:433
QDomDocument * getBoardInfo(void)
Definition: vboxutils.cpp:195
static QString getFirstText(QDomElement &element)
Definition: vboxutils.cpp:440
vbox_chan_map_t * getChannels(void)
Definition: vboxutils.cpp:298
QStringList getTuners(void)
returns a list of tuners in the format 'TUNERNO TUNERTYPE' eg '1 DVBT/T2'
Definition: vboxutils.cpp:273
static bool sendQuery(const QString &query, QDomDocument *xmlDoc)
Definition: vboxutils.cpp:360
static QStringList probeDevices(void)
Definition: vboxutils.cpp:31
static QString getStrValue(const QDomElement &element, const QString &name, int index=0)
Definition: vboxutils.cpp:419
QString m_url
Definition: vboxutils.h:50
VBox(QString url)
Definition: vboxutils.h:19
bool checkConnection(void)
Definition: vboxutils.cpp:211
static QString getIPFromVideoDevice(const QString &dev)
Definition: vboxutils.cpp:151
static QStringList doUPNPSearch(void)
Definition: vboxutils.cpp:68
unsigned int uint
Definition: compat.h:68
#define minor(X)
Definition: compat.h:66
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
string version
Definition: giantbomb.py:185
QMap< QString, DeviceLocation * > EntryMap
Key == Unique Service Name (USN)
Definition: ssdpcache.h:34
QMap< QString, VBoxChannelInfo > vbox_chan_map_t
#define LOC
Definition: vboxutils.cpp:18
static constexpr std::chrono::milliseconds SEARCH_TIME
Definition: vboxutils.cpp:26
static constexpr const char * VBOX_UDN
Definition: vboxutils.cpp:28
static constexpr const char * QUERY_CHANNELS
Definition: vboxutils.cpp:23
static constexpr const char * QUERY_BOARDINFO
Definition: vboxutils.cpp:21
static constexpr const char * VBOX_URI
Definition: vboxutils.cpp:27
static constexpr const char * VBOX_MIN_API_VERSION
Definition: vboxutils.h:14