MythTV master
mythairplayserver.cpp
Go to the documentation of this file.
1// TODO
2// locking ?
3// race on startup?
4// http date format and locale
5
6#include <algorithm>
7#include <chrono>
8#include <vector>
9
10#include <QTcpSocket>
11#include <QNetworkInterface>
12#include <QCoreApplication>
13#include <QKeyEvent>
14#include <QCryptographicHash>
15#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
16#include <QStringConverter>
17#endif
18#include <QTimer>
19#include <QUrlQuery>
20
22#include "libmythbase/mthread.h"
31
32#include "mythairplayserver.h"
33#include "tv_actions.h"
34#include "tv_play.h"
35
38QRecursiveMutex* MythAirplayServer::gMythAirplayServerMutex = new QRecursiveMutex();
39
40#define LOC QString("AirPlay: ")
41
42static constexpr uint16_t HTTP_STATUS_OK { 200 };
44static constexpr uint16_t HTTP_STATUS_NOT_IMPLEMENTED { 501 };
45static constexpr uint16_t HTTP_STATUS_UNAUTHORIZED { 401 };
46static constexpr uint16_t HTTP_STATUS_NOT_FOUND { 404 };
47
48static constexpr const char* AIRPLAY_SERVER_VERSION_STR { "115.2" };
49static const QString SERVER_INFO { "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
50"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
51"<plist version=\"1.0\">\r\n"\
52"<dict>\r\n"\
53"<key>deviceid</key>\r\n"\
54"<string>%1</string>\r\n"\
55"<key>features</key>\r\n"\
56"<integer>119</integer>\r\n"\
57"<key>model</key>\r\n"\
58"<string>MythTV,1</string>\r\n"\
59"<key>protovers</key>\r\n"\
60"<string>1.0</string>\r\n"\
61"<key>srcvers</key>\r\n"\
62"<string>%1</string>\r\n"\
63"</dict>\r\n"\
64"</plist>\r\n" };
65
66static const QString EVENT_INFO { "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n" \
67"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n"\
68"<plist version=\"1.0\">\r\n"\
69"<dict>\r\n"\
70"<key>category</key>\r\n"\
71"<string>video</string>\r\n"\
72"<key>state</key>\r\n"\
73"<string>%1</string>\r\n"\
74"</dict>\r\n"\
75"</plist>\r\n" };
76
77static const QString PLAYBACK_INFO { "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
78"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
79"<plist version=\"1.0\">\r\n"\
80"<dict>\r\n"\
81"<key>duration</key>\r\n"\
82"<real>%1</real>\r\n"\
83"<key>loadedTimeRanges</key>\r\n"\
84"<array>\r\n"\
85"\t\t<dict>\r\n"\
86"\t\t\t<key>duration</key>\r\n"\
87"\t\t\t<real>%2</real>\r\n"\
88"\t\t\t<key>start</key>\r\n"\
89"\t\t\t<real>0.0</real>\r\n"\
90"\t\t</dict>\r\n"\
91"</array>\r\n"\
92"<key>playbackBufferEmpty</key>\r\n"\
93"<true/>\r\n"\
94"<key>playbackBufferFull</key>\r\n"\
95"<false/>\r\n"\
96"<key>playbackLikelyToKeepUp</key>\r\n"\
97"<true/>\r\n"\
98"<key>position</key>\r\n"\
99"<real>%3</real>\r\n"\
100"<key>rate</key>\r\n"\
101"<real>%4</real>\r\n"\
102"<key>readyToPlay</key>\r\n"\
103"<true/>\r\n"\
104"<key>seekableTimeRanges</key>\r\n"\
105"<array>\r\n"\
106"\t\t<dict>\r\n"\
107"\t\t\t<key>duration</key>\r\n"\
108"\t\t\t<real>%1</real>\r\n"\
109"\t\t\t<key>start</key>\r\n"\
110"\t\t\t<real>0.0</real>\r\n"\
111"\t\t</dict>\r\n"\
112"</array>\r\n"\
113"</dict>\r\n"\
114"</plist>\r\n" };
115
116static const QString NOT_READY { "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
117"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
118"<plist version=\"1.0\">\r\n"\
119"<dict>\r\n"\
120"<key>readyToPlay</key>\r\n"\
121"<false/>\r\n"\
122"</dict>\r\n"\
123"</plist>\r\n" };
124
126{
127 QString key = "AirPlayId";
128 QString id = gCoreContext->GetSetting(key);
129 int size = id.size();
130 if (size == 12 && id.toUpper() == id)
131 return id;
132 if (size != 12)
133 {
134 QByteArray ba;
135 for (size_t i = 0; i < AIRPLAY_HARDWARE_ID_SIZE; i++)
136 {
137 ba.append(MythRandom(33, 33 + 80 - 1));
138 }
139 id = ba.toHex();
140 }
141 id = id.toUpper();
142
143 gCoreContext->SaveSetting(key, id);
144 return id;
145}
146
147QString GenerateNonce(void)
148{
149 std::array<uint32_t,4> nonceParts {
150 MythRandom(),
151 MythRandom(),
152 MythRandom(),
153 MythRandom()
154 };
155
156 QString nonce;
157 nonce = QString::number(nonceParts[0], 16).toUpper();
158 nonce += QString::number(nonceParts[1], 16).toUpper();
159 nonce += QString::number(nonceParts[2], 16).toUpper();
160 nonce += QString::number(nonceParts[3], 16).toUpper();
161 return nonce;
162}
163
164QByteArray DigestMd5Response(const QString& response, const QString& option,
165 const QString& nonce, const QString& password,
166 QByteArray &auth)
167{
168 int authStart = response.indexOf("response=\"") + 10;
169 int authLength = response.indexOf("\"", authStart) - authStart;
170 auth = response.mid(authStart, authLength).toLatin1();
171
172 int uriStart = response.indexOf("uri=\"") + 5;
173 int uriLength = response.indexOf("\"", uriStart) - uriStart;
174 QByteArray uri = response.mid(uriStart, uriLength).toLatin1();
175
176 int userStart = response.indexOf("username=\"") + 10;
177 int userLength = response.indexOf("\"", userStart) - userStart;
178 QByteArray user = response.mid(userStart, userLength).toLatin1();
179
180 int realmStart = response.indexOf("realm=\"") + 7;
181 int realmLength = response.indexOf("\"", realmStart) - realmStart;
182 QByteArray realm = response.mid(realmStart, realmLength).toLatin1();
183
184 QByteArray passwd = password.toLatin1();
185
186 QCryptographicHash hash(QCryptographicHash::Md5);
187 QByteArray colon(":", 1);
188 hash.addData(user);
189 hash.addData(colon);
190 hash.addData(realm);
191 hash.addData(colon);
192 hash.addData(passwd);
193 QByteArray ha1 = hash.result();
194 ha1 = ha1.toHex();
195
196 // calculate H(A2)
197 hash.reset();
198 hash.addData(option.toLatin1());
199 hash.addData(colon);
200 hash.addData(uri);
201 QByteArray ha2 = hash.result().toHex();
202
203 // calculate response
204 hash.reset();
205 hash.addData(ha1);
206 hash.addData(colon);
207 hash.addData(nonce.toLatin1());
208 hash.addData(colon);
209 hash.addData(ha2);
210 return hash.result().toHex();
211}
212
213using RequestQuery = QPair<QByteArray, QByteArray>;
214
216{
217 public:
218 explicit APHTTPRequest(QByteArray& data) : m_data(data)
219 {
220 Process();
221 Check();
222 }
223 ~APHTTPRequest() = default;
224
225 QByteArray& GetMethod(void) { return m_method; }
226 QByteArray& GetURI(void) { return m_uri; }
227 QByteArray& GetBody(void) { return m_body; }
228 QMap<QByteArray,QByteArray>& GetHeaders(void)
229 { return m_headers; }
230
231 void Append(QByteArray& data)
232 {
233 m_body.append(data);
234 Check();
235 }
236
237 QByteArray GetQueryValue(const QByteArray& key)
238 {
239 auto query = std::ranges::find(std::as_const(m_queries), key,
240 &RequestQuery::first);
241 return (query != m_queries.cend()) ? query->second : "";
242 }
243
244 QMap<QByteArray,QByteArray> GetHeadersFromBody(void)
245 {
246 QMap<QByteArray,QByteArray> result;
247 QList<QByteArray> lines = m_body.split('\n');;
248 for (const QByteArray& line : std::as_const(lines))
249 {
250 int index = line.indexOf(":");
251 if (index > 0)
252 {
253 result.insert(line.left(index).trimmed(),
254 line.mid(index + 1).trimmed());
255 }
256 }
257 return result;
258 }
259
260 bool IsComplete(void) const
261 {
262 return !m_incomingPartial;
263 }
264
265 private:
266 QByteArray GetLine(void)
267 {
268 int next = m_data.indexOf("\r\n", m_readPos);
269 if (next < 0) return {};
270 QByteArray line = m_data.mid(m_readPos, next - m_readPos);
271 m_readPos = next + 2;
272 return line;
273 }
274
275 void Process(void)
276 {
277 if (m_data.isEmpty())
278 return;
279
280 // request line
281 QByteArray line = GetLine();
282 if (line.isEmpty())
283 return;
284 QList<QByteArray> vals = line.split(' ');
285 if (vals.size() < 3)
286 return;
287 m_method = vals[0].trimmed();
288 QUrl url = QUrl::fromEncoded(vals[1].trimmed());
289 m_uri = url.path(QUrl::FullyEncoded).toLocal8Bit();
290 m_queries.clear();
291 {
292 QList<QPair<QString, QString> > items =
293 QUrlQuery(url).queryItems(QUrl::FullyEncoded);
294 QList<QPair<QString, QString> >::ConstIterator it = items.constBegin();
295 for ( ; it != items.constEnd(); ++it)
296 m_queries << qMakePair(it->first.toLatin1(), it->second.toLatin1());
297 }
298 if (m_method.isEmpty() || m_uri.isEmpty())
299 return;
300
301 // headers
302 while (!(line = GetLine()).isEmpty())
303 {
304 int index = line.indexOf(":");
305 if (index > 0)
306 {
307 m_headers.insert(line.left(index).trimmed(),
308 line.mid(index + 1).trimmed());
309 }
310 }
311
312 // body?
313 if (m_headers.contains("Content-Length"))
314 {
315 int remaining = m_data.size() - m_readPos;
316 m_size = m_headers["Content-Length"].toInt();
317 if (m_size > 0 && remaining > 0)
318 {
320 m_readPos += m_body.size();
321 }
322 }
323 }
324
325 void Check(void)
326 {
328 {
329 LOG(VB_GENERAL, LOG_DEBUG, LOC +
330 QString("HTTP Request:\n%1").arg(m_data.data()));
331 }
332 if (m_body.size() < m_size)
333 {
334 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
335 QString("AP HTTPRequest: Didn't read entire buffer."
336 "Left to receive: %1 (got %2 of %3) body=%4")
337 .arg(m_size-m_body.size()).arg(m_readPos).arg(m_size).arg(m_body.size()));
338 m_incomingPartial = true;
339 return;
340 }
341 m_incomingPartial = false;
342 }
343
344 int m_readPos {0};
345 QByteArray m_data;
346 QByteArray m_method;
347 QByteArray m_uri;
348 QList<RequestQuery> m_queries;
349 QMap<QByteArray,QByteArray> m_headers;
350 QByteArray m_body;
351 int m_size {0};
352 bool m_incomingPartial {false};
353};
354
356{
357 QMutexLocker locker(gMythAirplayServerMutex);
358
359 // create the server thread
361 gMythAirplayServerThread = new MThread("AirplayServer");
363 {
364 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create airplay thread.");
365 return false;
366 }
367
368 // create the server object
372 {
373 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create airplay object.");
374 return false;
375 }
376
377 // start the thread
379 {
381 QObject::connect(
382 gMythAirplayServerThread->qthread(), &QThread::started,
384 QObject::connect(
385 gMythAirplayServerThread->qthread(), &QThread::finished,
387 gMythAirplayServerThread->start(QThread::LowestPriority);
388 }
389
390 LOG(VB_GENERAL, LOG_INFO, LOC + "Created airplay objects.");
391 return true;
392}
393
395{
396 LOG(VB_GENERAL, LOG_INFO, LOC + "Cleaning up.");
397
398 QMutexLocker locker(gMythAirplayServerMutex);
400 {
403 }
405 gMythAirplayServerThread = nullptr;
406
407 delete gMythAirplayServer;
408 gMythAirplayServer = nullptr;
409}
410
411
413{
414 delete m_lock;
415 m_lock = nullptr;
416}
417
419{
420 QMutexLocker locker(m_lock);
421
422 // invalidate
423 m_valid = false;
424
425 // stop Bonjour Service Updater
427 {
428 m_serviceRefresh->stop();
429 delete m_serviceRefresh;
430 m_serviceRefresh = nullptr;
431 }
432
433 // disconnect from mDNS
434 delete m_bonjour;
435 m_bonjour = nullptr;
436
437 // disconnect connections
438 for (QTcpSocket* connection : std::as_const(m_sockets))
439 {
440 disconnect(connection, nullptr, nullptr, nullptr);
441 delete connection;
442 }
443 m_sockets.clear();
444
445 // remove all incoming buffers
446 for (APHTTPRequest* request : std::as_const(m_incoming))
447 {
448 delete request;
449 }
450 m_incoming.clear();
451}
452
454{
455 QMutexLocker locker(m_lock);
456
457 // already started?
458 if (m_valid)
459 return;
460
461 // join the dots
462 connect(this, &ServerPool::newConnection,
464
465 // start listening for connections
466 // try a few ports in case the default is in use
467 int baseport = m_setupPort;
469 if (m_setupPort < 0)
470 {
471 LOG(VB_GENERAL, LOG_ERR, LOC +
472 "Failed to find a port for incoming connections.");
473 }
474 else
475 {
476 // announce service
477 m_bonjour = new BonjourRegister(this);
478 if (!m_bonjour)
479 {
480 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create Bonjour object.");
481 return;
482 }
483
484 // give each frontend a unique name
485 int multiple = m_setupPort - baseport;
486 if (multiple > 0)
487 m_name += QString::number(multiple);
488
489 QByteArray name = m_name.toUtf8();
490 name.append(" on ");
491 name.append(gCoreContext->GetHostName().toUtf8());
492 QByteArray type = "_airplay._tcp";
493 QByteArray txt;
494 txt.append(26); txt.append("deviceid="); txt.append(GetMacAddress().toUtf8());
495 // supposed to be: 0: video, 1:Phone, 3: Volume Control, 4: HLS
496 // 9: Audio, 10: ? (but important without it it fails) 11: Audio redundant
497 txt.append(13); txt.append("features=0xF7");
498 txt.append(14); txt.append("model=MythTV,1");
499 txt.append(13); txt.append("srcvers=").append(AIRPLAY_SERVER_VERSION_STR);
500
501 if (!m_bonjour->Register(m_setupPort, type, name, txt))
502 {
503 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service.");
504 return;
505 }
506 if (!m_serviceRefresh)
507 {
508 m_serviceRefresh = new QTimer();
510 }
511 // Will force a Bonjour refresh in two seconds
512 m_serviceRefresh->start(2s);
513 }
514 m_valid = true;
515}
516
518{
520 m_serviceRefresh->start(10s);
521}
522
524{
525 Teardown();
526}
527
529{
530 QMutexLocker locker(m_lock);
531 LOG(VB_GENERAL, LOG_INFO, LOC + QString("New connection from %1:%2")
532 .arg(client->peerAddress().toString()).arg(client->peerPort()));
533
534 gCoreContext->SendSystemEvent(QString("AIRPLAY_NEW_CONNECTION"));
535 m_sockets.append(client);
536 connect(client, &QAbstractSocket::disconnected,
537 this, qOverload<>(&MythAirplayServer::deleteConnection));
538 connect(client, &QIODevice::readyRead, this, &MythAirplayServer::read);
539}
540
542{
543 QMutexLocker locker(m_lock);
544 auto *socket = qobject_cast<QTcpSocket *>(sender());
545 if (!socket)
546 return;
547
548 if (!m_sockets.contains(socket))
549 return;
550
551 deleteConnection(socket);
552}
553
555{
556 // must have lock
557 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing connection %1:%2")
558 .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
559 gCoreContext->SendSystemEvent(QString("AIRPLAY_DELETE_CONNECTION"));
560 m_sockets.removeOne(socket);
561
562 QByteArray remove;
563 for (auto it = m_connections.begin(); it != m_connections.end(); ++it)
564 {
565 if (it.value().m_reverseSocket == socket)
566 it.value().m_reverseSocket = nullptr;
567 if (it.value().m_controlSocket == socket)
568 it.value().m_controlSocket = nullptr;
569 if (!it.value().m_reverseSocket &&
570 !it.value().m_controlSocket)
571 {
572 if (!it.value().m_stopped)
573 {
574 StopSession(it.key());
575 }
576 remove = it.key();
577 break;
578 }
579 }
580
581 if (!remove.isEmpty())
582 {
583 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing session '%1'")
584 .arg(remove.data()));
585 m_connections.remove(remove);
586
587 MythNotification n(tr("Client disconnected"), tr("AirPlay"),
588 tr("from %1").arg(socket->peerAddress().toString()));
589 // Don't show it during playback
592 }
593
594 socket->deleteLater();
595
596 if (m_incoming.contains(socket))
597 {
598 delete m_incoming[socket];
599 m_incoming.remove(socket);
600 }
601}
602
604{
605 QMutexLocker locker(m_lock);
606 auto *socket = qobject_cast<QTcpSocket *>(sender());
607 if (!socket)
608 return;
609
610 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Read for %1:%2")
611 .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
612
613 QByteArray buf = socket->readAll();
614
615 if (!m_incoming.contains(socket))
616 {
617 auto *request = new APHTTPRequest(buf);
618 m_incoming.insert(socket, request);
619 }
620 else
621 {
622 m_incoming[socket]->Append(buf);
623 }
624 if (!m_incoming[socket]->IsComplete())
625 return;
626 HandleResponse(m_incoming[socket], socket);
627 if (m_incoming.contains(socket))
628 {
629 delete m_incoming[socket];
630 m_incoming.remove(socket);
631 }
632}
633
635{
636 switch (status)
637 {
638 case HTTP_STATUS_OK: return "OK";
639 case HTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching Protocols";
640 case HTTP_STATUS_NOT_IMPLEMENTED: return "Not Implemented";
641 case HTTP_STATUS_UNAUTHORIZED: return "Unauthorized";
642 case HTTP_STATUS_NOT_FOUND: return "Not Found";
643 }
644 return "";
645}
646
648 QTcpSocket *socket)
649{
650 if (!socket)
651 return;
652 QHostAddress addr = socket->peerAddress();
653 QByteArray session;
654 QByteArray header;
655 QString body;
656 uint16_t status = HTTP_STATUS_OK;
657 QByteArray content_type;
658
659 if (req->GetURI() != "/playback-info")
660 {
661 LOG(VB_GENERAL, LOG_INFO, LOC +
662 QString("Method: %1 URI: %2")
663 .arg(req->GetMethod().data(), req->GetURI().data()));
664 }
665 else
666 {
667 LOG(VB_GENERAL, LOG_DEBUG, LOC +
668 QString("Method: %1 URI: %2")
669 .arg(req->GetMethod().data(), req->GetURI().data()));
670 }
671
672 if (req->GetURI() == "200" || req->GetMethod().startsWith("HTTP"))
673 return;
674
675 if (!req->GetHeaders().contains("X-Apple-Session-ID"))
676 {
677 LOG(VB_GENERAL, LOG_DEBUG, LOC +
678 QString("No session ID in http request. "
679 "Connection from iTunes? Using IP %1").arg(addr.toString()));
680 }
681 else
682 {
683 session = req->GetHeaders()["X-Apple-Session-ID"];
684 }
685
686 if (session.size() == 0)
687 {
688 // No session ID, use IP address instead
689 session = addr.toString().toLatin1();
690 }
691 if (!m_connections.contains(session))
692 {
693 AirplayConnection apcon;
694 m_connections.insert(session, apcon);
695 }
696
697 if (req->GetURI() == "/reverse")
698 {
699 QTcpSocket *s = m_connections[session].m_reverseSocket;
700 if (s != socket && s != nullptr)
701 {
702 LOG(VB_GENERAL, LOG_ERR, LOC +
703 "Already have a different reverse socket for this connection.");
704 return;
705 }
706 m_connections[session].m_reverseSocket = socket;
708 header = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
709 SendResponse(socket, status, header, content_type, body);
710 return;
711 }
712
713 QTcpSocket *s = m_connections[session].m_controlSocket;
714 if (s != socket && s != nullptr)
715 {
716 LOG(VB_GENERAL, LOG_ERR, LOC +
717 "Already have a different control socket for this connection.");
718 return;
719 }
720 m_connections[session].m_controlSocket = socket;
721
722 if (m_connections[session].m_controlSocket != nullptr &&
723 m_connections[session].m_reverseSocket != nullptr &&
724 !m_connections[session].m_initialized)
725 {
726 // Got a full connection, disconnect any other clients
727 DisconnectAllClients(session);
728 m_connections[session].m_initialized = true;
729
730 MythNotification n(tr("New Connection"), tr("AirPlay"),
731 tr("from %1").arg(socket->peerAddress().toString()));
732 // Don't show it during playback
735 }
736
737 double position = 0.0;
738 double duration = 0.0;
739 float playerspeed = 0.0F;
740 bool playing = false;
741 QString pathname;
742 GetPlayerStatus(playing, playerspeed, position, duration, pathname);
743
744 if (playing && pathname != m_pathname)
745 {
746 // not ours
747 playing = false;
748 }
749 if (playing && duration > 0.01 && position < 0.01)
750 {
751 // Assume playback hasn't started yet, get saved position
752 position = m_connections[session].m_position;
753 }
754 if (!playing && m_connections[session].m_was_playing)
755 {
756 // playback got interrupted, notify client to stop
758 {
759 m_connections[session].m_was_playing = false;
760 }
761 }
762 else
763 {
764 m_connections[session].m_was_playing = playing;
765 }
766
767 if (gCoreContext->GetBoolSetting("AirPlayPasswordEnabled", false))
768 {
769 if (m_nonce.isEmpty())
770 {
772 }
773 header = QString("WWW-Authenticate: Digest realm=\"AirPlay\", "
774 "nonce=\"%1\"\r\n").arg(m_nonce).toLatin1();
775 if (!req->GetHeaders().contains("Authorization"))
776 {
778 header, content_type, body);
779 return;
780 }
781
782 QByteArray auth;
783 if (DigestMd5Response(req->GetHeaders()["Authorization"], req->GetMethod(), m_nonce,
784 gCoreContext->GetSetting("AirPlayPassword"),
785 auth) == auth)
786 {
787 LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay client authenticated");
788 }
789 else
790 {
791 LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay authentication failed");
793 header, content_type, body);
794 return;
795 }
796 header = "";
797 }
798
799 if (req->GetURI() == "/server-info")
800 {
801 content_type = "text/x-apple-plist+xml\r\n";
803 body.replace("%1", GetMacAddress());
804 LOG(VB_GENERAL, LOG_INFO, body);
805 }
806 else if (req->GetURI() == "/scrub")
807 {
808 double pos = req->GetQueryValue("position").toDouble();
809 if (req->GetMethod() == "POST")
810 {
811 // this may be received before playback starts...
812 auto intpos = (uint64_t)pos;
813 m_connections[session].m_position = pos;
814 LOG(VB_GENERAL, LOG_INFO, LOC +
815 QString("Scrub: (post) seek to %1").arg(intpos));
816 SeekPosition(intpos);
817 }
818 else if (req->GetMethod() == "GET")
819 {
820 content_type = "text/parameters\r\n";
821 body = QString("duration: %1\r\nposition: %2\r\n")
822 .arg(duration, 0, 'f', 6, '0')
823 .arg(position, 0, 'f', 6, '0');
824
825 LOG(VB_GENERAL, LOG_INFO, LOC +
826 QString("Scrub: (get) returned %1 of %2")
827 .arg(position).arg(duration));
828
829 /*
830 if (playing && playerspeed < 1.0F)
831 {
832 SendReverseEvent(session, AP_EVENT_PLAYING);
833 QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
834 Qt::NoModifier, ACTION_PLAY);
835 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
836 }
837 */
838 }
839 }
840 else if (req->GetURI() == "/stop")
841 {
842 StopSession(session);
843 }
844 else if (req->GetURI() == "/photo")
845 {
846 if (req->GetMethod() == "PUT")
847 {
848 // this may be received before playback starts...
849 QImage image = QImage::fromData(req->GetBody());
850 bool png =
851 req->GetBody().size() > 3 && req->GetBody()[1] == 'P' &&
852 req->GetBody()[2] == 'N' && req->GetBody()[3] == 'G';
853 LOG(VB_GENERAL, LOG_INFO, LOC +
854 QString("Received %1x%2 %3 photo")
855 .arg(image.width()).arg(image.height()).
856 arg(png ? "jpeg" : "png"));
857
858 if (m_connections[session].m_notificationid < 0)
859 {
860 m_connections[session].m_notificationid =
862 }
863 // send full screen display notification
865 n.SetId(m_connections[session].m_notificationid);
866 n.SetParent(this);
867 n.SetFullScreen(true);
869 // This is a photo session
870 m_connections[session].m_photos = true;
871 }
872 }
873 else if (req->GetURI() == "/slideshow-features")
874 {
875 LOG(VB_GENERAL, LOG_INFO, LOC +
876 "Slideshow functionality not implemented.");
877 }
878 else if (req->GetURI() == "/authorize")
879 {
880 LOG(VB_GENERAL, LOG_INFO, LOC + "Ignoring authorize request.");
881 }
882 else if ((req->GetURI() == "/setProperty") ||
883 (req->GetURI() == "/getProperty"))
884 {
885 status = HTTP_STATUS_NOT_FOUND;
886 }
887 else if (req->GetURI() == "/rate")
888 {
889 float rate = req->GetQueryValue("value").toFloat();
890 m_connections[session].m_speed = rate;
891
892 if (rate < 1.0F)
893 {
894 if (playerspeed > 0.0F)
895 {
897 }
899 }
900 else
901 {
902 if (playerspeed < 1.0F)
903 {
905 }
907 // If there's any photos left displayed, hide them
909 }
910 }
911 else if (req->GetURI() == "/play")
912 {
913 QByteArray file;
914 double start_pos = 0.0;
915 if (req->GetHeaders().contains("Content-Type") &&
916 req->GetHeaders()["Content-Type"] == "application/x-apple-binary-plist")
917 {
918 MythBinaryPList plist(req->GetBody());
919 LOG(VB_GENERAL, LOG_DEBUG, LOC + plist.ToString());
920
921 QVariant start = plist.GetValue("Start-Position");
922 QVariant content = plist.GetValue("Content-Location");
923 if (start.isValid() && start.canConvert<double>())
924 start_pos = start.toDouble();
925 if (content.isValid() && content.canConvert<QByteArray>())
926 file = content.toByteArray();
927 }
928 else
929 {
930 QMap<QByteArray,QByteArray> headers = req->GetHeadersFromBody();
931 file = headers["Content-Location"];
932 start_pos = headers["Start-Position"].toDouble();
933 }
934
935 if (!file.isEmpty())
936 {
937 m_pathname = QUrl::fromPercentEncoding(file);
939 GetPlayerStatus(playing, playerspeed, position, duration, pathname);
940 m_connections[session].m_url = QUrl(m_pathname);
941 m_connections[session].m_position = start_pos * duration;
942 if (TV::IsTVRunning())
943 {
945 }
946 if (duration * start_pos >= .1)
947 {
948 // not point seeking so close to the beginning
949 SeekPosition(duration * start_pos);
950 }
951 }
952
954 LOG(VB_GENERAL, LOG_INFO, LOC + QString("File: '%1' start_pos '%2'")
955 .arg(file.data()).arg(start_pos));
956 }
957 else if (req->GetURI() == "/playback-info")
958 {
959 content_type = "text/x-apple-plist+xml\r\n";
960
961 if (!playing)
962 {
963 body = NOT_READY;
965 }
966 else
967 {
968 body = PLAYBACK_INFO;
969 body.replace("%1", QString("%1").arg(duration, 0, 'f', 6, '0'));
970 body.replace("%2", QString("%1").arg(duration, 0, 'f', 6, '0')); // cached
971 body.replace("%3", QString("%1").arg(position, 0, 'f', 6, '0'));
972 body.replace("%4", playerspeed > 0.0F ? "1.0" : "0.0");
973 LOG(VB_GENERAL, LOG_DEBUG, body);
974 SendReverseEvent(session, playerspeed > 0.0F ? AP_EVENT_PLAYING :
976 }
977 }
978 SendResponse(socket, status, header, content_type, body);
979}
980
981void MythAirplayServer::SendResponse(QTcpSocket *socket,
982 uint16_t status, const QByteArray& header,
983 const QByteArray& content_type, const QString& body)
984{
985 if (!socket || !m_incoming.contains(socket) ||
986 socket->state() != QAbstractSocket::ConnectedState)
987 return;
988 QTextStream response(socket);
989#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
990 response.setCodec("UTF-8");
991#else
992 response.setEncoding(QStringConverter::Utf8);
993#endif
994 QByteArray reply;
995 reply.append("HTTP/1.1 ");
996 reply.append(QString::number(status).toUtf8());
997 reply.append(" ");
998 reply.append(StatusToString(status));
999 reply.append("\r\n");
1000 reply.append("DATE: ");
1001 reply.append(MythDate::current().toString("ddd, d MMM yyyy hh:mm:ss").toUtf8());
1002 reply.append(" GMT\r\n");
1003 if (!header.isEmpty())
1004 reply.append(header);
1005
1006 if (!body.isEmpty())
1007 {
1008 reply.append("Content-Type: ");
1009 reply.append(content_type);
1010 reply.append("Content-Length: ");
1011 reply.append(QString::number(body.size()).toUtf8());
1012 }
1013 else
1014 {
1015 reply.append("Content-Length: 0");
1016 }
1017 reply.append("\r\n\r\n");
1018
1019 if (!body.isEmpty())
1020 reply.append(body.toUtf8());
1021
1022 response << reply;
1023 response.flush();
1024
1025 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send: %1 \n\n%2\n")
1026 .arg(socket->flush()).arg(reply.data()));
1027}
1028
1030 AirplayEvent event)
1031{
1032 if (!m_connections.contains(session))
1033 return false;
1034 if (m_connections[session].m_lastEvent == event)
1035 return false;
1036 if (!m_connections[session].m_reverseSocket)
1037 return false;
1038
1039 QString body;
1040 if (AP_EVENT_PLAYING == event ||
1041 AP_EVENT_LOADING == event ||
1042 AP_EVENT_PAUSED == event ||
1043 AP_EVENT_STOPPED == event)
1044 {
1045 body = EVENT_INFO;
1046 body.replace("%1", eventToString(event));
1047 }
1048
1049 m_connections[session].m_lastEvent = event;
1050 QTextStream response(m_connections[session].m_reverseSocket);
1051#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1052 response.setCodec("UTF-8");
1053#else
1054 response.setEncoding(QStringConverter::Utf8);
1055#endif
1056 QByteArray reply;
1057 reply.append("POST /event HTTP/1.1\r\n");
1058 reply.append("Content-Type: text/x-apple-plist+xml\r\n");
1059 reply.append("Content-Length: ");
1060 reply.append(QString::number(body.size()).toUtf8());
1061 reply.append("\r\n");
1062 reply.append("x-apple-session-id: ");
1063 reply.append(session);
1064 reply.append("\r\n\r\n");
1065 if (!body.isEmpty())
1066 reply.append(body.toUtf8());
1067
1068 response << reply;
1069 response.flush();
1070
1071 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send reverse: %1 \n\n%2\n")
1072 .arg(m_connections[session].m_reverseSocket->flush())
1073 .arg(reply.data()));
1074 return true;
1075}
1076
1078{
1079 switch (event)
1080 {
1081 case AP_EVENT_PLAYING: return "playing";
1082 case AP_EVENT_PAUSED: return "paused";
1083 case AP_EVENT_LOADING: return "loading";
1084 case AP_EVENT_STOPPED: return "stopped";
1085 case AP_EVENT_NONE: return "none";
1086 default: return "";
1087 }
1088}
1089
1090void MythAirplayServer::GetPlayerStatus(bool &playing, float &speed,
1091 double &position, double &duration,
1092 QString &pathname)
1093{
1094 QVariantMap state;
1096
1097 if (state.contains("state"))
1098 playing = state["state"].toString() != "idle";
1099 if (state.contains("playspeed"))
1100 speed = state["playspeed"].toFloat();
1101 if (state.contains("secondsplayed"))
1102 position = state["secondsplayed"].toDouble();
1103 if (state.contains("totalseconds"))
1104 duration = state["totalseconds"].toDouble();
1105 if (state.contains("pathname"))
1106 pathname = state["pathname"].toString();
1107}
1108
1110{
1111 QString id = AirPlayHardwareId();
1112
1113 QString res;
1114 for (int i = 1; i <= id.size(); i++)
1115 {
1116 res.append(id[i-1]);
1117 if (i % 2 == 0 && i != id.size())
1118 {
1119 res.append(':');
1120 }
1121 }
1122 return res;
1123}
1124
1125void MythAirplayServer::StopSession(const QByteArray &session)
1126{
1127 AirplayConnection& cnx = m_connections[session];
1128
1129 if (cnx.m_photos)
1130 {
1131 if (cnx.m_notificationid > 0)
1132 {
1133 // close any photos that could be displayed
1135 cnx.m_notificationid = -1;
1136 }
1137 return;
1138 }
1139 cnx.m_stopped = true;
1140 double position = 0.0;
1141 double duration = 0.0;
1142 float playerspeed = 0.0F;
1143 bool playing = false;
1144 QString pathname;
1145 GetPlayerStatus(playing, playerspeed, position, duration, pathname);
1146 if (pathname != m_pathname)
1147 {
1148 // not ours
1149 return;
1150 }
1151 if (!playing)
1152 {
1153 return;
1154 }
1155 StopPlayback();
1156}
1157
1158void MythAirplayServer::DisconnectAllClients(const QByteArray &session)
1159{
1160 QMutexLocker locker(m_lock);
1161 QHash<QByteArray,AirplayConnection>::iterator it = m_connections.begin();
1162 AirplayConnection& current_cnx = m_connections[session];
1163
1164 while (it != m_connections.end())
1165 {
1166 AirplayConnection& cnx = it.value();
1167
1168 if (it.key() == session ||
1169 (current_cnx.m_reverseSocket && cnx.m_reverseSocket &&
1170 current_cnx.m_reverseSocket->peerAddress() == cnx.m_reverseSocket->peerAddress()) ||
1171 (current_cnx.m_controlSocket && cnx.m_controlSocket &&
1172 current_cnx.m_controlSocket->peerAddress() == cnx.m_controlSocket->peerAddress()))
1173 {
1174 // ignore if the connection is the currently active one or
1175 // from the same IP address
1176 ++it;
1177 continue;
1178 }
1179 if (!(*it).m_stopped)
1180 {
1181 StopSession(it.key());
1182 }
1183 QTcpSocket *socket = cnx.m_reverseSocket;
1184 if (socket)
1185 {
1186 socket->disconnect();
1187 socket->close();
1188 m_sockets.removeOne(socket);
1189 socket->deleteLater();
1190 if (m_incoming.contains(socket))
1191 {
1192 delete m_incoming[socket];
1193 m_incoming.remove(socket);
1194 }
1195 }
1196 socket = cnx.m_controlSocket;
1197 if (socket)
1198 {
1199 socket->disconnect();
1200 socket->close();
1201 m_sockets.removeOne(socket);
1202 socket->deleteLater();
1203 if (m_incoming.contains(socket))
1204 {
1205 delete m_incoming[socket];
1206 m_incoming.remove(socket);
1207 }
1208 }
1209 it = m_connections.erase(it);
1210 }
1211}
1212
1213void MythAirplayServer::StartPlayback(const QString &pathname)
1214{
1215 if (TV::IsTVRunning())
1216 {
1217 StopPlayback();
1218 }
1219 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1220 QString("Sending ACTION_HANDLEMEDIA for %1")
1221 .arg(pathname));
1222 auto* me = new MythEvent(ACTION_HANDLEMEDIA, QStringList(pathname));
1223 qApp->postEvent(GetMythMainWindow(), me);
1224 // Wait until we receive that the play has started
1225 std::vector<CoreWaitInfo> sigs {
1226 { "TVPlaybackStarted", &MythCoreContext::TVPlaybackStarted },
1227 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1229 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1230 QString("ACTION_HANDLEMEDIA completed"));
1231}
1232
1234{
1235 if (TV::IsTVRunning())
1236 {
1237 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1238 QString("Sending ACTION_STOP for %1")
1239 .arg(m_pathname));
1240
1241 auto* ke = new QKeyEvent(QEvent::KeyPress, 0,
1242 Qt::NoModifier, ACTION_STOP);
1243 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1244 // Wait until we receive that playback has stopped
1245 std::vector<CoreWaitInfo> sigs {
1246 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1247 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1249 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1250 QString("ACTION_STOP completed"));
1251 }
1252 else
1253 {
1254 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1255 QString("Playback not running, nothing to stop"));
1256 }
1257}
1258
1259void MythAirplayServer::SeekPosition(uint64_t position)
1260{
1261 if (TV::IsTVRunning())
1262 {
1263 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1264 QString("Sending ACTION_SEEKABSOLUTE(%1) for %2")
1265 .arg(position)
1266 .arg(m_pathname));
1267
1268 auto* me = new MythEvent(ACTION_SEEKABSOLUTE,
1269 QStringList(QString::number(position)));
1270 qApp->postEvent(GetMythMainWindow(), me);
1271 // Wait until we receive that the seek has completed
1272 std::vector<CoreWaitInfo> sigs {
1273 { "TVPlaybackSought", qOverload<>(&MythCoreContext::TVPlaybackSought) },
1274 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1275 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1277 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1278 QString("ACTION_SEEKABSOLUTE completed"));
1279 }
1280 else
1281 {
1282 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1283 QString("Trying to seek when playback hasn't started"));
1284 }
1285}
1286
1288{
1289 if (TV::IsTVRunning() && !TV::IsPaused())
1290 {
1291 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1292 QString("Sending ACTION_PAUSE for %1")
1293 .arg(m_pathname));
1294
1295 auto* ke = new QKeyEvent(QEvent::KeyPress, 0,
1296 Qt::NoModifier, ACTION_PAUSE);
1297 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1298 // Wait until we receive that playback has stopped
1299 std::vector<CoreWaitInfo> sigs {
1300 { "TVPlaybackPaused", &MythCoreContext::TVPlaybackPaused },
1301 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1302 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1304 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1305 QString("ACTION_PAUSE completed"));
1306 }
1307 else
1308 {
1309 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1310 QString("Playback not running, nothing to pause"));
1311 }
1312}
1313
1315{
1316 if (TV::IsTVRunning())
1317 {
1318 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1319 QString("Sending ACTION_PLAY for %1")
1320 .arg(m_pathname));
1321
1322 auto* ke = new QKeyEvent(QEvent::KeyPress, 0,
1323 Qt::NoModifier, ACTION_PLAY);
1324 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1325 // Wait until we receive that playback has stopped
1326 std::vector<CoreWaitInfo> sigs {
1327 { "TVPlaybackPlaying", &MythCoreContext::TVPlaybackPlaying },
1328 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1329 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1331 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1332 QString("ACTION_PLAY completed"));
1333 }
1334 else
1335 {
1336 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1337 QString("Playback not running, nothing to unpause"));
1338 }
1339}
1340
1342{
1343 // playback has started, dismiss any currently displayed photo
1344 QHash<QByteArray,AirplayConnection>::iterator it = m_connections.begin();
1345
1346 while (it != m_connections.end())
1347 {
1348 AirplayConnection& cnx = it.value();
1349
1350 if (cnx.m_photos)
1351 {
1352 cnx.UnRegister();
1353 }
1354 ++it;
1355 }
1356}
APHTTPRequest(QByteArray &data)
QMap< QByteArray, QByteArray > m_headers
bool IsComplete(void) const
QByteArray & GetMethod(void)
QMap< QByteArray, QByteArray > & GetHeaders(void)
QList< RequestQuery > m_queries
QByteArray & GetBody(void)
void Append(QByteArray &data)
QMap< QByteArray, QByteArray > GetHeadersFromBody(void)
QByteArray GetQueryValue(const QByteArray &key)
QByteArray & GetURI(void)
QByteArray GetLine(void)
~APHTTPRequest()=default
QTcpSocket * m_controlSocket
QTcpSocket * m_reverseSocket
bool ReAnnounceService(void)
bool Register(uint16_t port, const QByteArray &type, const QByteArray &name, const QByteArray &txt)
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
bool isRunning(void) const
Definition: mthread.cpp:247
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:267
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:284
void exit(int retcode=0)
Use this to exit from the thread if you are using a Qt event loop.
Definition: mthread.cpp:262
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:217
QRecursiveMutex * m_lock
QHash< QByteArray, AirplayConnection > m_connections
void StopSession(const QByteArray &session)
void newAirplayConnection(QTcpSocket *client)
static void GetPlayerStatus(bool &playing, float &speed, double &position, double &duration, QString &pathname)
void StartPlayback(const QString &pathname)
void HandleResponse(APHTTPRequest *req, QTcpSocket *socket)
static MythAirplayServer * gMythAirplayServer
void DisconnectAllClients(const QByteArray &session)
bool SendReverseEvent(QByteArray &session, AirplayEvent event)
static bool Create(void)
QHash< QTcpSocket *, APHTTPRequest * > m_incoming
static QByteArray StatusToString(uint16_t status)
static void Cleanup(void)
static QString GetMacAddress()
QList< QTcpSocket * > m_sockets
static MThread * gMythAirplayServerThread
BonjourRegister * m_bonjour
void SendResponse(QTcpSocket *socket, uint16_t status, const QByteArray &header, const QByteArray &content_type, const QString &body)
static QRecursiveMutex * gMythAirplayServerMutex
~MythAirplayServer(void) override
void SeekPosition(uint64_t position)
static QString eventToString(AirplayEvent event)
QVariant GetValue(const QString &Key)
QString GetHostName(void)
void SaveSetting(const QString &key, int newValue)
void TVPlaybackAborted(void)
QString GetSetting(const QString &key, const QString &defaultval="")
void SendSystemEvent(const QString &msg)
void TVPlaybackPaused(void)
void TVPlaybackSought(void)
void TVPlaybackStopped(void)
void TVPlaybackPlaying(void)
void WaitUntilSignals(std::vector< CoreWaitInfo > &sigs) const
Wait until any of the provided signals have been received.
void TVPlaybackStarted(void)
bool GetBoolSetting(const QString &key, bool defaultval=false)
This class is used as a container for messages.
Definition: mythevent.h:17
void UnRegister(void *from, int id, bool closeimemdiately=false)
Unregister the client.
int Register(void *from)
An application can register in which case it will be assigned a reusable screen, which can be modifie...
bool Queue(const MythNotification &notification)
Queue a notification Queue() is thread-safe and can be called from anywhere.
void SetVisibility(VNMask nVisibility)
Define a bitmask of Visibility.
void SetId(int Id)
Contains the application registration id.
static const Type kNew
void SetFullScreen(bool FullScreen)
A notification may request to be displayed in full screen, this request may not be fullfilled should ...
void SetParent(void *Parent)
Contains the parent address. Required if id is set Id provided must match the parent address as provi...
VNMask GetVisibility() const
static void GetFreshState(QVariantMap &State)
int tryListeningPort(int baseport, int range=1)
tryListeningPort
Definition: serverpool.cpp:730
void newConnection(QTcpSocket *)
static bool IsTVRunning()
Check whether media is currently playing.
Definition: tv_play.cpp:176
static bool IsPaused()
Check whether playback is paused.
Definition: tv_play.cpp:4894
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)
unsigned short uint16_t
Definition: iso6937tables.h:3
static const QString NOT_READY
#define LOC
QString AirPlayHardwareId()
QPair< QByteArray, QByteArray > RequestQuery
static const QString SERVER_INFO
static constexpr uint16_t HTTP_STATUS_UNAUTHORIZED
static constexpr const char * AIRPLAY_SERVER_VERSION_STR
static constexpr uint16_t HTTP_STATUS_OK
static constexpr uint16_t HTTP_STATUS_SWITCHING_PROTOCOLS
static const QString PLAYBACK_INFO
static constexpr uint16_t HTTP_STATUS_NOT_FOUND
QByteArray DigestMd5Response(const QString &response, const QString &option, const QString &nonce, const QString &password, QByteArray &auth)
QString GenerateNonce(void)
static const QString EVENT_INFO
static constexpr uint16_t HTTP_STATUS_NOT_IMPLEMENTED
static constexpr int AIRPLAY_PORT_RANGE
static constexpr size_t AIRPLAY_HARDWARE_ID_SIZE
AirplayEvent
@ AP_EVENT_STOPPED
@ AP_EVENT_LOADING
@ AP_EVENT_PAUSED
@ AP_EVENT_NONE
@ AP_EVENT_PLAYING
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythNotificationCenter * GetNotificationCenter(void)
MythMainWindow * GetMythMainWindow(void)
Convenience inline random number generator functions.
static constexpr const char * ACTION_HANDLEMEDIA
Definition: mythuiactions.h:21
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
uint32_t MythRandom()
generate 32 random bits
Definition: mythrandom.h:20
static floatvec vals
Definition: tentacle3d.cpp:21
#define ACTION_PLAY
Definition: tv_actions.h:30
#define ACTION_PAUSE
Definition: tv_actions.h:15
#define ACTION_SEEKABSOLUTE
Definition: tv_actions.h:40
#define ACTION_STOP
Definition: tv_actions.h:8