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