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