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 for (auto it = m_connections.begin(); it != m_connections.end(); ++it)
561 {
562 if (it.value().m_reverseSocket == socket)
563 it.value().m_reverseSocket = nullptr;
564 if (it.value().m_controlSocket == socket)
565 it.value().m_controlSocket = nullptr;
566 if (!it.value().m_reverseSocket &&
567 !it.value().m_controlSocket)
568 {
569 if (!it.value().m_stopped)
570 {
571 StopSession(it.key());
572 }
573 remove = it.key();
574 break;
575 }
576 }
577
578 if (!remove.isEmpty())
579 {
580 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing session '%1'")
581 .arg(remove.data()));
582 m_connections.remove(remove);
583
584 MythNotification n(tr("Client disconnected"), tr("AirPlay"),
585 tr("from %1").arg(socket->peerAddress().toString()));
586 // Don't show it during playback
589 }
590
591 socket->deleteLater();
592
593 if (m_incoming.contains(socket))
594 {
595 delete m_incoming[socket];
596 m_incoming.remove(socket);
597 }
598}
599
601{
602 QMutexLocker locker(m_lock);
603 auto *socket = qobject_cast<QTcpSocket *>(sender());
604 if (!socket)
605 return;
606
607 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Read for %1:%2")
608 .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
609
610 QByteArray buf = socket->readAll();
611
612 if (!m_incoming.contains(socket))
613 {
614 auto *request = new APHTTPRequest(buf);
615 m_incoming.insert(socket, request);
616 }
617 else
618 {
619 m_incoming[socket]->Append(buf);
620 }
621 if (!m_incoming[socket]->IsComplete())
622 return;
623 HandleResponse(m_incoming[socket], socket);
624 if (m_incoming.contains(socket))
625 {
626 delete m_incoming[socket];
627 m_incoming.remove(socket);
628 }
629}
630
632{
633 switch (status)
634 {
635 case HTTP_STATUS_OK: return "OK";
636 case HTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching Protocols";
637 case HTTP_STATUS_NOT_IMPLEMENTED: return "Not Implemented";
638 case HTTP_STATUS_UNAUTHORIZED: return "Unauthorized";
639 case HTTP_STATUS_NOT_FOUND: return "Not Found";
640 }
641 return "";
642}
643
645 QTcpSocket *socket)
646{
647 if (!socket)
648 return;
649 QHostAddress addr = socket->peerAddress();
650 QByteArray session;
651 QByteArray header;
652 QString body;
653 uint16_t status = HTTP_STATUS_OK;
654 QByteArray content_type;
655
656 if (req->GetURI() != "/playback-info")
657 {
658 LOG(VB_GENERAL, LOG_INFO, LOC +
659 QString("Method: %1 URI: %2")
660 .arg(req->GetMethod().data(), req->GetURI().data()));
661 }
662 else
663 {
664 LOG(VB_GENERAL, LOG_DEBUG, LOC +
665 QString("Method: %1 URI: %2")
666 .arg(req->GetMethod().data(), req->GetURI().data()));
667 }
668
669 if (req->GetURI() == "200" || req->GetMethod().startsWith("HTTP"))
670 return;
671
672 if (!req->GetHeaders().contains("X-Apple-Session-ID"))
673 {
674 LOG(VB_GENERAL, LOG_DEBUG, LOC +
675 QString("No session ID in http request. "
676 "Connection from iTunes? Using IP %1").arg(addr.toString()));
677 }
678 else
679 {
680 session = req->GetHeaders()["X-Apple-Session-ID"];
681 }
682
683 if (session.size() == 0)
684 {
685 // No session ID, use IP address instead
686 session = addr.toString().toLatin1();
687 }
688 if (!m_connections.contains(session))
689 {
690 AirplayConnection apcon;
691 m_connections.insert(session, apcon);
692 }
693
694 if (req->GetURI() == "/reverse")
695 {
696 QTcpSocket *s = m_connections[session].m_reverseSocket;
697 if (s != socket && s != nullptr)
698 {
699 LOG(VB_GENERAL, LOG_ERR, LOC +
700 "Already have a different reverse socket for this connection.");
701 return;
702 }
703 m_connections[session].m_reverseSocket = socket;
705 header = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
706 SendResponse(socket, status, header, content_type, body);
707 return;
708 }
709
710 QTcpSocket *s = m_connections[session].m_controlSocket;
711 if (s != socket && s != nullptr)
712 {
713 LOG(VB_GENERAL, LOG_ERR, LOC +
714 "Already have a different control socket for this connection.");
715 return;
716 }
717 m_connections[session].m_controlSocket = socket;
718
719 if (m_connections[session].m_controlSocket != nullptr &&
720 m_connections[session].m_reverseSocket != nullptr &&
721 !m_connections[session].m_initialized)
722 {
723 // Got a full connection, disconnect any other clients
724 DisconnectAllClients(session);
725 m_connections[session].m_initialized = true;
726
727 MythNotification n(tr("New Connection"), tr("AirPlay"),
728 tr("from %1").arg(socket->peerAddress().toString()));
729 // Don't show it during playback
732 }
733
734 double position = 0.0;
735 double duration = 0.0;
736 float playerspeed = 0.0F;
737 bool playing = false;
738 QString pathname;
739 GetPlayerStatus(playing, playerspeed, position, duration, pathname);
740
741 if (playing && pathname != m_pathname)
742 {
743 // not ours
744 playing = false;
745 }
746 if (playing && duration > 0.01 && position < 0.01)
747 {
748 // Assume playback hasn't started yet, get saved position
749 position = m_connections[session].m_position;
750 }
751 if (!playing && m_connections[session].m_was_playing)
752 {
753 // playback got interrupted, notify client to stop
755 {
756 m_connections[session].m_was_playing = false;
757 }
758 }
759 else
760 {
761 m_connections[session].m_was_playing = playing;
762 }
763
764 if (gCoreContext->GetBoolSetting("AirPlayPasswordEnabled", false))
765 {
766 if (m_nonce.isEmpty())
767 {
769 }
770 header = QString("WWW-Authenticate: Digest realm=\"AirPlay\", "
771 "nonce=\"%1\"\r\n").arg(m_nonce).toLatin1();
772 if (!req->GetHeaders().contains("Authorization"))
773 {
775 header, content_type, body);
776 return;
777 }
778
779 QByteArray auth;
780 if (DigestMd5Response(req->GetHeaders()["Authorization"], req->GetMethod(), m_nonce,
781 gCoreContext->GetSetting("AirPlayPassword"),
782 auth) == auth)
783 {
784 LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay client authenticated");
785 }
786 else
787 {
788 LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay authentication failed");
790 header, content_type, body);
791 return;
792 }
793 header = "";
794 }
795
796 if (req->GetURI() == "/server-info")
797 {
798 content_type = "text/x-apple-plist+xml\r\n";
800 body.replace("%1", GetMacAddress());
801 LOG(VB_GENERAL, LOG_INFO, body);
802 }
803 else if (req->GetURI() == "/scrub")
804 {
805 double pos = req->GetQueryValue("position").toDouble();
806 if (req->GetMethod() == "POST")
807 {
808 // this may be received before playback starts...
809 auto intpos = (uint64_t)pos;
810 m_connections[session].m_position = pos;
811 LOG(VB_GENERAL, LOG_INFO, LOC +
812 QString("Scrub: (post) seek to %1").arg(intpos));
813 SeekPosition(intpos);
814 }
815 else if (req->GetMethod() == "GET")
816 {
817 content_type = "text/parameters\r\n";
818 body = QString("duration: %1\r\nposition: %2\r\n")
819 .arg(duration, 0, 'f', 6, '0')
820 .arg(position, 0, 'f', 6, '0');
821
822 LOG(VB_GENERAL, LOG_INFO, LOC +
823 QString("Scrub: (get) returned %1 of %2")
824 .arg(position).arg(duration));
825
826 /*
827 if (playing && playerspeed < 1.0F)
828 {
829 SendReverseEvent(session, AP_EVENT_PLAYING);
830 QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
831 Qt::NoModifier, ACTION_PLAY);
832 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
833 }
834 */
835 }
836 }
837 else if (req->GetURI() == "/stop")
838 {
839 StopSession(session);
840 }
841 else if (req->GetURI() == "/photo")
842 {
843 if (req->GetMethod() == "PUT")
844 {
845 // this may be received before playback starts...
846 QImage image = QImage::fromData(req->GetBody());
847 bool png =
848 req->GetBody().size() > 3 && req->GetBody()[1] == 'P' &&
849 req->GetBody()[2] == 'N' && req->GetBody()[3] == 'G';
850 LOG(VB_GENERAL, LOG_INFO, LOC +
851 QString("Received %1x%2 %3 photo")
852 .arg(image.width()).arg(image.height()).
853 arg(png ? "jpeg" : "png"));
854
855 if (m_connections[session].m_notificationid < 0)
856 {
857 m_connections[session].m_notificationid =
859 }
860 // send full screen display notification
862 n.SetId(m_connections[session].m_notificationid);
863 n.SetParent(this);
864 n.SetFullScreen(true);
866 // This is a photo session
867 m_connections[session].m_photos = true;
868 }
869 }
870 else if (req->GetURI() == "/slideshow-features")
871 {
872 LOG(VB_GENERAL, LOG_INFO, LOC +
873 "Slideshow functionality not implemented.");
874 }
875 else if (req->GetURI() == "/authorize")
876 {
877 LOG(VB_GENERAL, LOG_INFO, LOC + "Ignoring authorize request.");
878 }
879 else if ((req->GetURI() == "/setProperty") ||
880 (req->GetURI() == "/getProperty"))
881 {
882 status = HTTP_STATUS_NOT_FOUND;
883 }
884 else if (req->GetURI() == "/rate")
885 {
886 float rate = req->GetQueryValue("value").toFloat();
887 m_connections[session].m_speed = rate;
888
889 if (rate < 1.0F)
890 {
891 if (playerspeed > 0.0F)
892 {
894 }
896 }
897 else
898 {
899 if (playerspeed < 1.0F)
900 {
902 }
904 // If there's any photos left displayed, hide them
906 }
907 }
908 else if (req->GetURI() == "/play")
909 {
910 QByteArray file;
911 double start_pos = 0.0;
912 if (req->GetHeaders().contains("Content-Type") &&
913 req->GetHeaders()["Content-Type"] == "application/x-apple-binary-plist")
914 {
915 MythBinaryPList plist(req->GetBody());
916 LOG(VB_GENERAL, LOG_DEBUG, LOC + plist.ToString());
917
918 QVariant start = plist.GetValue("Start-Position");
919 QVariant content = plist.GetValue("Content-Location");
920 if (start.isValid() && start.canConvert<double>())
921 start_pos = start.toDouble();
922 if (content.isValid() && content.canConvert<QByteArray>())
923 file = content.toByteArray();
924 }
925 else
926 {
927 QMap<QByteArray,QByteArray> headers = req->GetHeadersFromBody();
928 file = headers["Content-Location"];
929 start_pos = headers["Start-Position"].toDouble();
930 }
931
932 if (!file.isEmpty())
933 {
934 m_pathname = QUrl::fromPercentEncoding(file);
936 GetPlayerStatus(playing, playerspeed, position, duration, pathname);
937 m_connections[session].m_url = QUrl(m_pathname);
938 m_connections[session].m_position = start_pos * duration;
939 if (TV::IsTVRunning())
940 {
942 }
943 if (duration * start_pos >= .1)
944 {
945 // not point seeking so close to the beginning
946 SeekPosition(duration * start_pos);
947 }
948 }
949
951 LOG(VB_GENERAL, LOG_INFO, LOC + QString("File: '%1' start_pos '%2'")
952 .arg(file.data()).arg(start_pos));
953 }
954 else if (req->GetURI() == "/playback-info")
955 {
956 content_type = "text/x-apple-plist+xml\r\n";
957
958 if (!playing)
959 {
960 body = NOT_READY;
962 }
963 else
964 {
965 body = PLAYBACK_INFO;
966 body.replace("%1", QString("%1").arg(duration, 0, 'f', 6, '0'));
967 body.replace("%2", QString("%1").arg(duration, 0, 'f', 6, '0')); // cached
968 body.replace("%3", QString("%1").arg(position, 0, 'f', 6, '0'));
969 body.replace("%4", playerspeed > 0.0F ? "1.0" : "0.0");
970 LOG(VB_GENERAL, LOG_DEBUG, body);
971 SendReverseEvent(session, playerspeed > 0.0F ? AP_EVENT_PLAYING :
973 }
974 }
975 SendResponse(socket, status, header, content_type, body);
976}
977
978void MythAirplayServer::SendResponse(QTcpSocket *socket,
979 uint16_t status, const QByteArray& header,
980 const QByteArray& content_type, const QString& body)
981{
982 if (!socket || !m_incoming.contains(socket) ||
983 socket->state() != QAbstractSocket::ConnectedState)
984 return;
985 QTextStream response(socket);
986#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
987 response.setCodec("UTF-8");
988#else
989 response.setEncoding(QStringConverter::Utf8);
990#endif
991 QByteArray reply;
992 reply.append("HTTP/1.1 ");
993 reply.append(QString::number(status).toUtf8());
994 reply.append(" ");
995 reply.append(StatusToString(status));
996 reply.append("\r\n");
997 reply.append("DATE: ");
998 reply.append(MythDate::current().toString("ddd, d MMM yyyy hh:mm:ss").toUtf8());
999 reply.append(" GMT\r\n");
1000 if (!header.isEmpty())
1001 reply.append(header);
1002
1003 if (!body.isEmpty())
1004 {
1005 reply.append("Content-Type: ");
1006 reply.append(content_type);
1007 reply.append("Content-Length: ");
1008 reply.append(QString::number(body.size()).toUtf8());
1009 }
1010 else
1011 {
1012 reply.append("Content-Length: 0");
1013 }
1014 reply.append("\r\n\r\n");
1015
1016 if (!body.isEmpty())
1017 reply.append(body.toUtf8());
1018
1019 response << reply;
1020 response.flush();
1021
1022 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send: %1 \n\n%2\n")
1023 .arg(socket->flush()).arg(reply.data()));
1024}
1025
1027 AirplayEvent event)
1028{
1029 if (!m_connections.contains(session))
1030 return false;
1031 if (m_connections[session].m_lastEvent == event)
1032 return false;
1033 if (!m_connections[session].m_reverseSocket)
1034 return false;
1035
1036 QString body;
1037 if (AP_EVENT_PLAYING == event ||
1038 AP_EVENT_LOADING == event ||
1039 AP_EVENT_PAUSED == event ||
1040 AP_EVENT_STOPPED == event)
1041 {
1042 body = EVENT_INFO;
1043 body.replace("%1", eventToString(event));
1044 }
1045
1046 m_connections[session].m_lastEvent = event;
1047 QTextStream response(m_connections[session].m_reverseSocket);
1048#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1049 response.setCodec("UTF-8");
1050#else
1051 response.setEncoding(QStringConverter::Utf8);
1052#endif
1053 QByteArray reply;
1054 reply.append("POST /event HTTP/1.1\r\n");
1055 reply.append("Content-Type: text/x-apple-plist+xml\r\n");
1056 reply.append("Content-Length: ");
1057 reply.append(QString::number(body.size()).toUtf8());
1058 reply.append("\r\n");
1059 reply.append("x-apple-session-id: ");
1060 reply.append(session);
1061 reply.append("\r\n\r\n");
1062 if (!body.isEmpty())
1063 reply.append(body.toUtf8());
1064
1065 response << reply;
1066 response.flush();
1067
1068 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send reverse: %1 \n\n%2\n")
1069 .arg(m_connections[session].m_reverseSocket->flush())
1070 .arg(reply.data()));
1071 return true;
1072}
1073
1075{
1076 switch (event)
1077 {
1078 case AP_EVENT_PLAYING: return "playing";
1079 case AP_EVENT_PAUSED: return "paused";
1080 case AP_EVENT_LOADING: return "loading";
1081 case AP_EVENT_STOPPED: return "stopped";
1082 case AP_EVENT_NONE: return "none";
1083 default: return "";
1084 }
1085}
1086
1087void MythAirplayServer::GetPlayerStatus(bool &playing, float &speed,
1088 double &position, double &duration,
1089 QString &pathname)
1090{
1091 QVariantMap state;
1093
1094 if (state.contains("state"))
1095 playing = state["state"].toString() != "idle";
1096 if (state.contains("playspeed"))
1097 speed = state["playspeed"].toFloat();
1098 if (state.contains("secondsplayed"))
1099 position = state["secondsplayed"].toDouble();
1100 if (state.contains("totalseconds"))
1101 duration = state["totalseconds"].toDouble();
1102 if (state.contains("pathname"))
1103 pathname = state["pathname"].toString();
1104}
1105
1107{
1108 QString id = AirPlayHardwareId();
1109
1110 QString res;
1111 for (int i = 1; i <= id.size(); i++)
1112 {
1113 res.append(id[i-1]);
1114 if (i % 2 == 0 && i != id.size())
1115 {
1116 res.append(':');
1117 }
1118 }
1119 return res;
1120}
1121
1122void MythAirplayServer::StopSession(const QByteArray &session)
1123{
1124 AirplayConnection& cnx = m_connections[session];
1125
1126 if (cnx.m_photos)
1127 {
1128 if (cnx.m_notificationid > 0)
1129 {
1130 // close any photos that could be displayed
1132 cnx.m_notificationid = -1;
1133 }
1134 return;
1135 }
1136 cnx.m_stopped = true;
1137 double position = 0.0;
1138 double duration = 0.0;
1139 float playerspeed = 0.0F;
1140 bool playing = false;
1141 QString pathname;
1142 GetPlayerStatus(playing, playerspeed, position, duration, pathname);
1143 if (pathname != m_pathname)
1144 {
1145 // not ours
1146 return;
1147 }
1148 if (!playing)
1149 {
1150 return;
1151 }
1152 StopPlayback();
1153}
1154
1155void MythAirplayServer::DisconnectAllClients(const QByteArray &session)
1156{
1157 QMutexLocker locker(m_lock);
1158 QHash<QByteArray,AirplayConnection>::iterator it = m_connections.begin();
1159 AirplayConnection& current_cnx = m_connections[session];
1160
1161 while (it != m_connections.end())
1162 {
1163 AirplayConnection& cnx = it.value();
1164
1165 if (it.key() == session ||
1166 (current_cnx.m_reverseSocket && cnx.m_reverseSocket &&
1167 current_cnx.m_reverseSocket->peerAddress() == cnx.m_reverseSocket->peerAddress()) ||
1168 (current_cnx.m_controlSocket && cnx.m_controlSocket &&
1169 current_cnx.m_controlSocket->peerAddress() == cnx.m_controlSocket->peerAddress()))
1170 {
1171 // ignore if the connection is the currently active one or
1172 // from the same IP address
1173 ++it;
1174 continue;
1175 }
1176 if (!(*it).m_stopped)
1177 {
1178 StopSession(it.key());
1179 }
1180 QTcpSocket *socket = cnx.m_reverseSocket;
1181 if (socket)
1182 {
1183 socket->disconnect();
1184 socket->close();
1185 m_sockets.removeOne(socket);
1186 socket->deleteLater();
1187 if (m_incoming.contains(socket))
1188 {
1189 delete m_incoming[socket];
1190 m_incoming.remove(socket);
1191 }
1192 }
1193 socket = cnx.m_controlSocket;
1194 if (socket)
1195 {
1196 socket->disconnect();
1197 socket->close();
1198 m_sockets.removeOne(socket);
1199 socket->deleteLater();
1200 if (m_incoming.contains(socket))
1201 {
1202 delete m_incoming[socket];
1203 m_incoming.remove(socket);
1204 }
1205 }
1206 it = m_connections.erase(it);
1207 }
1208}
1209
1210void MythAirplayServer::StartPlayback(const QString &pathname)
1211{
1212 if (TV::IsTVRunning())
1213 {
1214 StopPlayback();
1215 }
1216 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1217 QString("Sending ACTION_HANDLEMEDIA for %1")
1218 .arg(pathname));
1219 auto* me = new MythEvent(ACTION_HANDLEMEDIA, QStringList(pathname));
1220 qApp->postEvent(GetMythMainWindow(), me);
1221 // Wait until we receive that the play has started
1222 std::vector<CoreWaitInfo> sigs {
1223 { "TVPlaybackStarted", &MythCoreContext::TVPlaybackStarted },
1224 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1226 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1227 QString("ACTION_HANDLEMEDIA completed"));
1228}
1229
1231{
1232 if (TV::IsTVRunning())
1233 {
1234 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1235 QString("Sending ACTION_STOP for %1")
1236 .arg(m_pathname));
1237
1238 auto* ke = new QKeyEvent(QEvent::KeyPress, 0,
1239 Qt::NoModifier, ACTION_STOP);
1240 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1241 // Wait until we receive that playback has stopped
1242 std::vector<CoreWaitInfo> sigs {
1243 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1244 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1246 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1247 QString("ACTION_STOP completed"));
1248 }
1249 else
1250 {
1251 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1252 QString("Playback not running, nothing to stop"));
1253 }
1254}
1255
1256void MythAirplayServer::SeekPosition(uint64_t position)
1257{
1258 if (TV::IsTVRunning())
1259 {
1260 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1261 QString("Sending ACTION_SEEKABSOLUTE(%1) for %2")
1262 .arg(position)
1263 .arg(m_pathname));
1264
1265 auto* me = new MythEvent(ACTION_SEEKABSOLUTE,
1266 QStringList(QString::number(position)));
1267 qApp->postEvent(GetMythMainWindow(), me);
1268 // Wait until we receive that the seek has completed
1269 std::vector<CoreWaitInfo> sigs {
1270 { "TVPlaybackSought", qOverload<>(&MythCoreContext::TVPlaybackSought) },
1271 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1272 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1274 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1275 QString("ACTION_SEEKABSOLUTE completed"));
1276 }
1277 else
1278 {
1279 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1280 QString("Trying to seek when playback hasn't started"));
1281 }
1282}
1283
1285{
1286 if (TV::IsTVRunning() && !TV::IsPaused())
1287 {
1288 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1289 QString("Sending ACTION_PAUSE for %1")
1290 .arg(m_pathname));
1291
1292 auto* ke = new QKeyEvent(QEvent::KeyPress, 0,
1293 Qt::NoModifier, ACTION_PAUSE);
1294 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1295 // Wait until we receive that playback has stopped
1296 std::vector<CoreWaitInfo> sigs {
1297 { "TVPlaybackPaused", &MythCoreContext::TVPlaybackPaused },
1298 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1299 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1301 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1302 QString("ACTION_PAUSE completed"));
1303 }
1304 else
1305 {
1306 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1307 QString("Playback not running, nothing to pause"));
1308 }
1309}
1310
1312{
1313 if (TV::IsTVRunning())
1314 {
1315 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1316 QString("Sending ACTION_PLAY for %1")
1317 .arg(m_pathname));
1318
1319 auto* ke = new QKeyEvent(QEvent::KeyPress, 0,
1320 Qt::NoModifier, ACTION_PLAY);
1321 qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1322 // Wait until we receive that playback has stopped
1323 std::vector<CoreWaitInfo> sigs {
1324 { "TVPlaybackPlaying", &MythCoreContext::TVPlaybackPlaying },
1325 { "TVPlaybackStopped", &MythCoreContext::TVPlaybackStopped },
1326 { "TVPlaybackAborted", &MythCoreContext::TVPlaybackAborted } };
1328 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1329 QString("ACTION_PLAY completed"));
1330 }
1331 else
1332 {
1333 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1334 QString("Playback not running, nothing to unpause"));
1335 }
1336}
1337
1339{
1340 // playback has started, dismiss any currently displayed photo
1341 QHash<QByteArray,AirplayConnection>::iterator it = m_connections.begin();
1342
1343 while (it != m_connections.end())
1344 {
1345 AirplayConnection& cnx = it.value();
1346
1347 if (cnx.m_photos)
1348 {
1349 cnx.UnRegister();
1350 }
1351 ++it;
1352 }
1353}
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:261
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:281
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:298
void exit(int retcode=0)
Use this to exit from the thread if you are using a Qt event loop.
Definition: mthread.cpp:276
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:231
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:4888
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: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