10 #include <QNetworkInterface>
11 #include <QCoreApplication>
13 #include <QCryptographicHash>
14 #if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
15 #include <QStringConverter>
37 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
43 #define LOC QString("AirPlay: ")
52 static const QString
SERVER_INFO {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
53 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
54 "<plist version=\"1.0\">\r\n"\
56 "<key>deviceid</key>\r\n"\
57 "<string>%1</string>\r\n"\
58 "<key>features</key>\r\n"\
59 "<integer>119</integer>\r\n"\
60 "<key>model</key>\r\n"\
61 "<string>MythTV,1</string>\r\n"\
62 "<key>protovers</key>\r\n"\
63 "<string>1.0</string>\r\n"\
64 "<key>srcvers</key>\r\n"\
65 "<string>%1</string>\r\n"\
69 static const QString
EVENT_INFO {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n" \
70 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n"\
71 "<plist version=\"1.0\">\r\n"\
73 "<key>category</key>\r\n"\
74 "<string>video</string>\r\n"\
75 "<key>state</key>\r\n"\
76 "<string>%1</string>\r\n"\
80 static const QString
PLAYBACK_INFO {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
81 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
82 "<plist version=\"1.0\">\r\n"\
84 "<key>duration</key>\r\n"\
85 "<real>%1</real>\r\n"\
86 "<key>loadedTimeRanges</key>\r\n"\
89 "\t\t\t<key>duration</key>\r\n"\
90 "\t\t\t<real>%2</real>\r\n"\
91 "\t\t\t<key>start</key>\r\n"\
92 "\t\t\t<real>0.0</real>\r\n"\
95 "<key>playbackBufferEmpty</key>\r\n"\
97 "<key>playbackBufferFull</key>\r\n"\
99 "<key>playbackLikelyToKeepUp</key>\r\n"\
101 "<key>position</key>\r\n"\
102 "<real>%3</real>\r\n"\
103 "<key>rate</key>\r\n"\
104 "<real>%4</real>\r\n"\
105 "<key>readyToPlay</key>\r\n"\
107 "<key>seekableTimeRanges</key>\r\n"\
110 "\t\t\t<key>duration</key>\r\n"\
111 "\t\t\t<real>%1</real>\r\n"\
112 "\t\t\t<key>start</key>\r\n"\
113 "\t\t\t<real>0.0</real>\r\n"\
119 static const QString
NOT_READY {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
120 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
121 "<plist version=\"1.0\">\r\n"\
123 "<key>readyToPlay</key>\r\n"\
130 QString key =
"AirPlayId";
132 int size =
id.size();
133 if (size == 12 &&
id.toUpper() ==
id)
152 std::array<uint32_t,4> nonceParts {
160 nonce = QString::number(nonceParts[0], 16).toUpper();
161 nonce += QString::number(nonceParts[1], 16).toUpper();
162 nonce += QString::number(nonceParts[2], 16).toUpper();
163 nonce += QString::number(nonceParts[3], 16).toUpper();
168 const QString& nonce,
const QString& password,
171 int authStart = response.indexOf(
"response=\"") + 10;
172 int authLength = response.indexOf(
"\"", authStart) - authStart;
173 auth = response.mid(authStart, authLength).toLatin1();
175 int uriStart = response.indexOf(
"uri=\"") + 5;
176 int uriLength = response.indexOf(
"\"", uriStart) - uriStart;
177 QByteArray uri = response.mid(uriStart, uriLength).toLatin1();
179 int userStart = response.indexOf(
"username=\"") + 10;
180 int userLength = response.indexOf(
"\"", userStart) - userStart;
181 QByteArray
user = response.mid(userStart, userLength).toLatin1();
183 int realmStart = response.indexOf(
"realm=\"") + 7;
184 int realmLength = response.indexOf(
"\"", realmStart) - realmStart;
185 QByteArray realm = response.mid(realmStart, realmLength).toLatin1();
187 QByteArray passwd = password.toLatin1();
189 QCryptographicHash hash(QCryptographicHash::Md5);
191 hash.addData(
":", 1);
193 hash.addData(
":", 1);
194 hash.addData(passwd);
195 QByteArray ha1 = hash.result();
200 hash.addData(option.toLatin1());
201 hash.addData(
":", 1);
203 QByteArray ha2 = hash.result().toHex();
208 hash.addData(
":", 1);
209 hash.addData(nonce.toLatin1());
210 hash.addData(
":", 1);
212 return hash.result().toHex();
239 auto samekey = [key](
const auto& query) {
return query.first == key; };;
241 return (query !=
m_queries.cend()) ? query->second :
"";
246 QMap<QByteArray,QByteArray> result;
247 QList<QByteArray> lines =
m_body.split(
'\n');;
248 for (
const QByteArray& line : qAsConst(lines))
250 int index = line.indexOf(
":");
253 result.insert(line.left(index).trimmed(),
254 line.mid(index + 1).trimmed());
269 if (next < 0)
return {};
284 QList<QByteArray>
vals = line.split(
' ');
288 QUrl url = QUrl::fromEncoded(
vals[1].trimmed());
289 m_uri = url.path(QUrl::FullyEncoded).toLocal8Bit();
292 QList<QPair<QString, QString> > items =
293 QUrlQuery(url).queryItems(QUrl::FullyEncoded);
294 QList<QPair<QString, QString> >::ConstIterator it = items.constBegin();
295 for ( ; it != items.constEnd(); ++it)
296 m_queries << qMakePair(it->first.toLatin1(), it->second.toLatin1());
302 while (!(line =
GetLine()).isEmpty())
304 int index = line.indexOf(
":");
307 m_headers.insert(line.left(index).trimmed(),
308 line.mid(index + 1).trimmed());
313 if (
m_headers.contains(
"Content-Length"))
317 if (
m_size > 0 && remaining > 0)
329 LOG(VB_GENERAL, LOG_DEBUG,
LOC +
330 QString(
"HTTP Request:\n%1").arg(
m_data.data()));
334 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
335 QString(
"AP HTTPRequest: Didn't read entire buffer."
336 "Left to receive: %1 (got %2 of %3) body=%4")
364 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Failed to create airplay thread.");
373 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Failed to create airplay object.");
390 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Created airplay objects.");
396 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Cleaning up.");
420 QMutexLocker locker(
m_lock);
438 for (QTcpSocket* connection : qAsConst(
m_sockets))
440 disconnect(connection,
nullptr,
nullptr,
nullptr);
455 QMutexLocker locker(
m_lock);
471 LOG(VB_GENERAL, LOG_ERR,
LOC +
472 "Failed to find a port for incoming connections.");
480 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Failed to create Bonjour object.");
487 m_name += QString::number(multiple);
489 QByteArray name =
m_name.toUtf8();
492 QByteArray
type =
"_airplay._tcp";
494 txt.append(26); txt.append(
"deviceid="); txt.append(
GetMacAddress().toUtf8());
497 txt.append(13); txt.append(
"features=0xF7");
498 txt.append(14); txt.append(
"model=MythTV,1");
503 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Failed to register service.");
530 QMutexLocker locker(
m_lock);
531 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"New connection from %1:%2")
532 .arg(client->peerAddress().toString()).arg(client->peerPort()));
536 connect(client, &QAbstractSocket::disconnected,
543 QMutexLocker locker(
m_lock);
544 auto *socket = qobject_cast<QTcpSocket *>(sender());
557 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"Removing connection %1:%2")
558 .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
563 QMutableHashIterator<QByteArray,AirplayConnection> it(
m_connections);
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)
574 if (!it.value().m_stopped)
583 if (!remove.isEmpty())
585 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"Removing session '%1'")
586 .arg(remove.data()));
590 tr(
"from %1").arg(socket->peerAddress().toString()));
596 socket->deleteLater();
607 QMutexLocker locker(
m_lock);
608 auto *socket = qobject_cast<QTcpSocket *>(sender());
612 LOG(VB_GENERAL, LOG_DEBUG,
LOC + QString(
"Read for %1:%2")
613 .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
615 QByteArray buf = socket->readAll();
654 QHostAddress addr = socket->peerAddress();
659 QByteArray content_type;
661 if (req->
GetURI() !=
"/playback-info")
663 LOG(VB_GENERAL, LOG_INFO,
LOC +
664 QString(
"Method: %1 URI: %2")
669 LOG(VB_GENERAL, LOG_DEBUG,
LOC +
670 QString(
"Method: %1 URI: %2")
677 if (!req->
GetHeaders().contains(
"X-Apple-Session-ID"))
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()));
685 session = req->
GetHeaders()[
"X-Apple-Session-ID"];
688 if (session.size() == 0)
691 session = addr.toString().toLatin1();
699 if (req->
GetURI() ==
"/reverse")
702 if (s != socket && s !=
nullptr)
704 LOG(VB_GENERAL, LOG_ERR,
LOC +
705 "Already have a different reverse socket for this connection.");
710 header =
"Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
711 SendResponse(socket, status, header, content_type, body);
716 if (s != socket && s !=
nullptr)
718 LOG(VB_GENERAL, LOG_ERR,
LOC +
719 "Already have a different control socket for this connection.");
733 tr(
"from %1").arg(socket->peerAddress().toString()));
739 double position = 0.0F;
740 double duration = 0.0F;
741 float playerspeed = 0.0F;
742 bool playing =
false;
751 if (playing && duration > 0.01 && position < 0.01)
775 header = QString(
"WWW-Authenticate: Digest realm=\"AirPlay\", "
776 "nonce=\"%1\"\r\n").arg(
m_nonce).toLatin1();
777 if (!req->
GetHeaders().contains(
"Authorization"))
780 header, content_type, body);
789 LOG(VB_GENERAL, LOG_INFO,
LOC +
"AirPlay client authenticated");
793 LOG(VB_GENERAL, LOG_INFO,
LOC +
"AirPlay authentication failed");
795 header, content_type, body);
801 if (req->
GetURI() ==
"/server-info")
803 content_type =
"text/x-apple-plist+xml\r\n";
806 LOG(VB_GENERAL, LOG_INFO, body);
808 else if (req->
GetURI() ==
"/scrub")
814 auto intpos = (uint64_t)pos;
816 LOG(VB_GENERAL, LOG_INFO,
LOC +
817 QString(
"Scrub: (post) seek to %1").arg(intpos));
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');
827 LOG(VB_GENERAL, LOG_INFO,
LOC +
828 QString(
"Scrub: (get) returned %1 of %2")
829 .arg(position).arg(duration));
842 else if (req->
GetURI() ==
"/stop")
846 else if (req->
GetURI() ==
"/photo")
851 QImage image = QImage::fromData(req->
GetBody());
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"));
875 else if (req->
GetURI() ==
"/slideshow-features")
877 LOG(VB_GENERAL, LOG_INFO,
LOC +
878 "Slideshow functionality not implemented.");
880 else if (req->
GetURI() ==
"/authorize")
882 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Ignoring authorize request.");
884 else if ((req->
GetURI() ==
"/setProperty") ||
885 (req->
GetURI() ==
"/getProperty"))
889 else if (req->
GetURI() ==
"/rate")
896 if (playerspeed > 0.0F)
904 if (playerspeed < 1.0F)
913 else if (req->
GetURI() ==
"/play")
916 double start_pos = 0.0F;
917 if (req->
GetHeaders().contains(
"Content-Type") &&
918 req->
GetHeaders()[
"Content-Type"] ==
"application/x-apple-binary-plist")
923 QVariant start = plist.
GetValue(
"Start-Position");
925 if (start.isValid() && start.canConvert<
double>())
926 start_pos = start.toDouble();
933 file = headers[
"Content-Location"];
934 start_pos = headers[
"Start-Position"].toDouble();
948 if (duration * start_pos >= .1)
956 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"File: '%1' start_pos '%2'")
957 .arg(
file.data()).arg(start_pos));
959 else if (req->
GetURI() ==
"/playback-info")
961 content_type =
"text/x-apple-plist+xml\r\n";
971 body.replace(
"%1", QString(
"%1").arg(duration, 0,
'f', 6,
'0'));
972 body.replace(
"%2", QString(
"%1").arg(duration, 0,
'f', 6,
'0'));
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);
980 SendResponse(socket, status, header, content_type, body);
984 uint16_t status,
const QByteArray& header,
985 const QByteArray& content_type,
const QString& body)
987 if (!socket || !
m_incoming.contains(socket) ||
988 socket->state() != QAbstractSocket::ConnectedState)
990 QTextStream response(socket);
991 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
992 response.setCodec(
"UTF-8");
994 response.setEncoding(QStringConverter::Utf8);
997 reply.append(
"HTTP/1.1 ");
998 reply.append(QString::number(status).toUtf8());
1001 reply.append(
"\r\n");
1002 reply.append(
"DATE: ");
1004 reply.append(
" GMT\r\n");
1005 if (!header.isEmpty())
1006 reply.append(header);
1008 if (!body.isEmpty())
1010 reply.append(
"Content-Type: ");
1011 reply.append(content_type);
1012 reply.append(
"Content-Length: ");
1013 reply.append(QString::number(body.size()).toUtf8());
1017 reply.append(
"Content-Length: 0");
1019 reply.append(
"\r\n\r\n");
1021 if (!body.isEmpty())
1022 reply.append(body.toUtf8());
1027 LOG(VB_GENERAL, LOG_DEBUG,
LOC + QString(
"Send: %1 \n\n%2\n")
1028 .arg(socket->flush()).arg(reply.data()));
1052 QTextStream response(
m_connections[session].m_reverseSocket);
1053 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1054 response.setCodec(
"UTF-8");
1056 response.setEncoding(QStringConverter::Utf8);
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());
1073 LOG(VB_GENERAL, LOG_DEBUG,
LOC + QString(
"Send reverse: %1 \n\n%2\n")
1075 .arg(reply.data()));
1093 double &position,
double &duration,
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();
1116 for (
int i = 1; i <=
id.size(); i++)
1118 res.append(
id[i-1]);
1119 if (i % 2 == 0 && i !=
id.size())
1142 double position = 0.0F;
1143 double duration = 0.0F;
1144 float playerspeed = 0.0F;
1145 bool playing =
false;
1162 QMutexLocker locker(
m_lock);
1163 QHash<QByteArray,AirplayConnection>::iterator it =
m_connections.begin();
1170 if (it.key() == session ||
1181 if (!(*it).m_stopped)
1188 socket->disconnect();
1191 socket->deleteLater();
1201 socket->disconnect();
1204 socket->deleteLater();
1221 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1222 QString(
"Sending ACTION_HANDLEMEDIA for %1")
1227 std::vector<CoreWaitInfo> sigs {
1231 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1232 QString(
"ACTION_HANDLEMEDIA completed"));
1239 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1240 QString(
"Sending ACTION_STOP for %1")
1243 auto* ke =
new QKeyEvent(QEvent::KeyPress, 0,
1247 std::vector<CoreWaitInfo> sigs {
1251 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1252 QString(
"ACTION_STOP completed"));
1256 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1257 QString(
"Playback not running, nothing to stop"));
1265 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1266 QString(
"Sending ACTION_SEEKABSOLUTE(%1) for %2")
1271 QStringList(QString::number(position)));
1274 std::vector<CoreWaitInfo> sigs {
1279 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1280 QString(
"ACTION_SEEKABSOLUTE completed"));
1284 LOG(VB_PLAYBACK, LOG_WARNING,
LOC +
1285 QString(
"Trying to seek when playback hasn't started"));
1293 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1294 QString(
"Sending ACTION_PAUSE for %1")
1297 auto* ke =
new QKeyEvent(QEvent::KeyPress, 0,
1301 std::vector<CoreWaitInfo> sigs {
1306 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1307 QString(
"ACTION_PAUSE completed"));
1311 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1312 QString(
"Playback not running, nothing to pause"));
1320 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1321 QString(
"Sending ACTION_PLAY for %1")
1324 auto* ke =
new QKeyEvent(QEvent::KeyPress, 0,
1328 std::vector<CoreWaitInfo> sigs {
1333 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1334 QString(
"ACTION_PLAY completed"));
1338 LOG(VB_PLAYBACK, LOG_DEBUG,
LOC +
1339 QString(
"Playback not running, nothing to unpause"));
1346 QHash<QByteArray,AirplayConnection>::iterator it =
m_connections.begin();