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