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 <QTcpSocket>
7 #include <QNetworkInterface>
8 #include <QCoreApplication>
9 #include <QKeyEvent>
10 #include <QCryptographicHash>
11 #include <QTimer>
12 #include <QUrlQuery>
13 
14 #include "mthread.h"
15 #include "mythdate.h"
16 #include "mythlogging.h"
17 #include "mythcorecontext.h"
18 #include "mythuiactions.h"
19 #include "mythuistatetracker.h"
20 #include "plist.h"
21 #include "tv_play.h"
22 #include "mythmainwindow.h"
23 #include "tv_actions.h"
24 
25 #include "bonjourregister.h"
26 #include "mythairplayserver.h"
27 
30 QMutex* MythAirplayServer::gMythAirplayServerMutex = new QMutex(QMutex::Recursive);
31 
32 #define LOC QString("AirPlay: ")
33 
34 #define HTTP_STATUS_OK 200
35 #define HTTP_STATUS_SWITCHING_PROTOCOLS 101
36 #define HTTP_STATUS_NOT_IMPLEMENTED 501
37 #define HTTP_STATUS_UNAUTHORIZED 401
38 #define HTTP_STATUS_NOT_FOUND 404
39 
40 #define AIRPLAY_SERVER_VERSION_STR "115.2"
41 #define SERVER_INFO QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
42 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
43 "<plist version=\"1.0\">\r\n"\
44 "<dict>\r\n"\
45 "<key>deviceid</key>\r\n"\
46 "<string>%1</string>\r\n"\
47 "<key>features</key>\r\n"\
48 "<integer>119</integer>\r\n"\
49 "<key>model</key>\r\n"\
50 "<string>MythTV,1</string>\r\n"\
51 "<key>protovers</key>\r\n"\
52 "<string>1.0</string>\r\n"\
53 "<key>srcvers</key>\r\n"\
54 "<string>" AIRPLAY_SERVER_VERSION_STR "</string>\r\n"\
55 "</dict>\r\n"\
56 "</plist>\r\n")
57 
58 #define EVENT_INFO QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n"\
59 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n"\
60 "<plist version=\"1.0\">\r\n"\
61 "<dict>\r\n"\
62 "<key>category</key>\r\n"\
63 "<string>video</string>\r\n"\
64 "<key>state</key>\r\n"\
65 "<string>%1</string>\r\n"\
66 "</dict>\r\n"\
67 "</plist>\r\n")
68 
69 #define PLAYBACK_INFO QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
70 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
71 "<plist version=\"1.0\">\r\n"\
72 "<dict>\r\n"\
73 "<key>duration</key>\r\n"\
74 "<real>%1</real>\r\n"\
75 "<key>loadedTimeRanges</key>\r\n"\
76 "<array>\r\n"\
77 "\t\t<dict>\r\n"\
78 "\t\t\t<key>duration</key>\r\n"\
79 "\t\t\t<real>%2</real>\r\n"\
80 "\t\t\t<key>start</key>\r\n"\
81 "\t\t\t<real>0.0</real>\r\n"\
82 "\t\t</dict>\r\n"\
83 "</array>\r\n"\
84 "<key>playbackBufferEmpty</key>\r\n"\
85 "<true/>\r\n"\
86 "<key>playbackBufferFull</key>\r\n"\
87 "<false/>\r\n"\
88 "<key>playbackLikelyToKeepUp</key>\r\n"\
89 "<true/>\r\n"\
90 "<key>position</key>\r\n"\
91 "<real>%3</real>\r\n"\
92 "<key>rate</key>\r\n"\
93 "<real>%4</real>\r\n"\
94 "<key>readyToPlay</key>\r\n"\
95 "<true/>\r\n"\
96 "<key>seekableTimeRanges</key>\r\n"\
97 "<array>\r\n"\
98 "\t\t<dict>\r\n"\
99 "\t\t\t<key>duration</key>\r\n"\
100 "\t\t\t<real>%1</real>\r\n"\
101 "\t\t\t<key>start</key>\r\n"\
102 "\t\t\t<real>0.0</real>\r\n"\
103 "\t\t</dict>\r\n"\
104 "</array>\r\n"\
105 "</dict>\r\n"\
106 "</plist>\r\n")
107 
108 #define NOT_READY QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
109 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
110 "<plist version=\"1.0\">\r\n"\
111 "<dict>\r\n"\
112 "<key>readyToPlay</key>\r\n"\
113 "<false/>\r\n"\
114 "</dict>\r\n"\
115 "</plist>\r\n")
116 
118 {
119  QString key = "AirPlayId";
120  QString id = gCoreContext->GetSetting(key);
121  int size = id.size();
122  if (size == 12 && id.toUpper() == id)
123  return id;
124  if (size != 12)
125  {
126  QByteArray ba;
127  for (int i = 0; i < AIRPLAY_HARDWARE_ID_SIZE; i++)
128  ba.append((random() % 80) + 33);
129  id = ba.toHex();
130  }
131  id = id.toUpper();
132 
133  gCoreContext->SaveSetting(key, id);
134  return id;
135 }
136 
137 QString GenerateNonce(void)
138 {
139  int nonceParts[4];
140  QString nonce;
141 #if QT_VERSION >= QT_VERSION_CHECK(5,10,0)
142  auto randgen = QRandomGenerator::global();
143  nonceParts[0] = randgen->generate();
144  nonceParts[1] = randgen->generate();
145  nonceParts[2] = randgen->generate();
146  nonceParts[3] = randgen->generate();
147 #else
148  QTime time = QTime::currentTime();
149  qsrand((uint)time.msec());
150  nonceParts[0] = qrand();
151  nonceParts[1] = qrand();
152  nonceParts[2] = qrand();
153  nonceParts[3] = qrand();
154 #endif
155 
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 
163 QByteArray 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  hash.addData(user);
187  hash.addData(":", 1);
188  hash.addData(realm);
189  hash.addData(":", 1);
190  hash.addData(passwd);
191  QByteArray ha1 = hash.result();
192  ha1 = ha1.toHex();
193 
194  // calculate H(A2)
195  hash.reset();
196  hash.addData(option.toLatin1());
197  hash.addData(":", 1);
198  hash.addData(uri);
199  QByteArray ha2 = hash.result().toHex();
200 
201  // calculate response
202  hash.reset();
203  hash.addData(ha1);
204  hash.addData(":", 1);
205  hash.addData(nonce.toLatin1());
206  hash.addData(":", 1);
207  hash.addData(ha2);
208  return hash.result().toHex();
209 }
210 
212 {
213  public:
214  explicit APHTTPRequest(QByteArray& data) : m_data(data)
215  {
216  Process();
217  Check();
218  }
219  ~APHTTPRequest() = default;
220 
221  QByteArray& GetMethod(void) { return m_method; }
222  QByteArray& GetURI(void) { return m_uri; }
223  QByteArray& GetBody(void) { return m_body; }
224  QMap<QByteArray,QByteArray>& GetHeaders(void)
225  { return m_headers; }
226 
227  void Append(QByteArray& data)
228  {
229  m_body.append(data);
230  Check();
231  }
232 
233  QByteArray GetQueryValue(const QByteArray& key)
234  {
235  for (int i = 0; i < m_queries.size(); i++)
236  if (m_queries[i].first == key)
237  return m_queries[i].second;
238  return "";
239  }
240 
241  QMap<QByteArray,QByteArray> GetHeadersFromBody(void)
242  {
243  QMap<QByteArray,QByteArray> result;
244  QList<QByteArray> lines = m_body.split('\n');;
245  foreach (QByteArray line, 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)
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 QByteArray();
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.size())
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  {
316  m_body = m_data.mid(m_readPos, m_size);
317  m_readPos += m_body.size();
318  }
319  }
320  }
321 
322  void Check(void)
323  {
324  if (!m_incomingPartial)
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
366  if (!gMythAirplayServer)
368  if (!gMythAirplayServer)
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(), SIGNAL(started()),
380  gMythAirplayServer, SLOT(Start()));
381  QObject::connect(
382  gMythAirplayServerThread->qthread(), SIGNAL(finished()),
383  gMythAirplayServer, SLOT(Stop()));
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
423  if (m_serviceRefresh)
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  foreach (QTcpSocket* connection, m_sockets)
436  {
437  disconnect(connection, nullptr, nullptr, nullptr);
438  delete connection;
439  }
440  m_sockets.clear();
441 
442  // remove all incoming buffers
443  foreach (APHTTPRequest* request, 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, SIGNAL(newConnection(QTcpSocket*)),
460  this, SLOT(newConnection(QTcpSocket*)));
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());
489  QByteArray type = "_airplay._tcp";
490  QByteArray txt;
491  txt.append(26); txt.append("deviceid="); txt.append(GetMacAddress());
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=" 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();
506  connect(m_serviceRefresh, SIGNAL(timeout()), this, SLOT(timeout()));
507  }
508  // Will force a Bonjour refresh in two seconds
509  m_serviceRefresh->start(2000);
510  }
511  m_valid = true;
512 }
513 
515 {
517  m_serviceRefresh->start(10000);
518 }
519 
521 {
522  Teardown();
523 }
524 
525 void MythAirplayServer::newConnection(QTcpSocket *client)
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, SIGNAL(disconnected()), this, SLOT(deleteConnection()));
534  connect(client, SIGNAL(readyRead()), this, SLOT(read()));
535 }
536 
538 {
539  QMutexLocker locker(m_lock);
540  QTcpSocket *socket = (QTcpSocket *)sender();
541  if (!socket)
542  return;
543 
544  if (!m_sockets.contains(socket))
545  return;
546 
547  deleteConnection(socket);
548 }
549 
550 void MythAirplayServer::deleteConnection(QTcpSocket *socket)
551 {
552  // must have lock
553  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing connection %1:%2")
554  .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
555  gCoreContext->SendSystemEvent(QString("AIRPLAY_DELETE_CONNECTION"));
556  m_sockets.removeOne(socket);
557 
558  QByteArray remove;
559  QMutableHashIterator<QByteArray,AirplayConnection> it(m_connections);
560  while (it.hasNext())
561  {
562  it.next();
563  if (it.value().m_reverseSocket == socket)
564  it.value().m_reverseSocket = nullptr;
565  if (it.value().m_controlSocket == socket)
566  it.value().m_controlSocket = nullptr;
567  if (!it.value().m_reverseSocket &&
568  !it.value().m_controlSocket)
569  {
570  if (!it.value().m_stopped)
571  {
572  StopSession(it.key());
573  }
574  remove = it.key();
575  break;
576  }
577  }
578 
579  if (!remove.isEmpty())
580  {
581  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing session '%1'")
582  .arg(remove.data()));
583  m_connections.remove(remove);
584 
585  MythNotification n(tr("Client disconnected"), tr("AirPlay"),
586  tr("from %1").arg(socket->peerAddress().toString()));
587  // Don't show it during playback
588  n.SetVisibility(n.GetVisibility() & ~MythNotification::kPlayback);
590  }
591 
592  socket->deleteLater();
593 
594  if (m_incoming.contains(socket))
595  {
596  delete m_incoming[socket];
597  m_incoming.remove(socket);
598  }
599 }
600 
602 {
603  QMutexLocker locker(m_lock);
604  QTcpSocket *socket = (QTcpSocket *)sender();
605  if (!socket)
606  return;
607 
608  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Read for %1:%2")
609  .arg(socket->peerAddress().toString()).arg(socket->peerPort()));
610 
611  QByteArray buf = socket->readAll();
612 
613  if (!m_incoming.contains(socket))
614  {
615  APHTTPRequest *request = new APHTTPRequest(buf);
616  m_incoming.insert(socket, request);
617  }
618  else
619  {
620  m_incoming[socket]->Append(buf);
621  }
622  if (!m_incoming[socket]->IsComplete())
623  return;
624  HandleResponse(m_incoming[socket], socket);
625  if (m_incoming.contains(socket))
626  {
627  delete m_incoming[socket];
628  m_incoming.remove(socket);
629  }
630 }
631 
632 QByteArray MythAirplayServer::StatusToString(int status)
633 {
634  switch (status)
635  {
636  case HTTP_STATUS_OK: return "OK";
637  case HTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching Protocols";
638  case HTTP_STATUS_NOT_IMPLEMENTED: return "Not Implemented";
639  case HTTP_STATUS_UNAUTHORIZED: return "Unauthorized";
640  case HTTP_STATUS_NOT_FOUND: return "Not Found";
641  }
642  return "";
643 }
644 
646  QTcpSocket *socket)
647 {
648  if (!socket)
649  return;
650  QHostAddress addr = socket->peerAddress();
651  QByteArray session;
652  QByteArray header;
653  QString body;
654  int status = HTTP_STATUS_OK;
655  QByteArray content_type;
656 
657  if (req->GetURI() != "/playback-info")
658  {
659  LOG(VB_GENERAL, LOG_INFO, LOC +
660  QString("Method: %1 URI: %2")
661  .arg(req->GetMethod().data()).arg(req->GetURI().data()));
662  }
663  else
664  {
665  LOG(VB_GENERAL, LOG_DEBUG, LOC +
666  QString("Method: %1 URI: %2")
667  .arg(req->GetMethod().data()).arg(req->GetURI().data()));
668  }
669 
670  if (req->GetURI() == "200" || req->GetMethod().startsWith("HTTP"))
671  return;
672 
673  if (!req->GetHeaders().contains("X-Apple-Session-ID"))
674  {
675  LOG(VB_GENERAL, LOG_DEBUG, LOC +
676  QString("No session ID in http request. "
677  "Connection from iTunes? Using IP %1").arg(addr.toString()));
678  }
679  else
680  {
681  session = req->GetHeaders()["X-Apple-Session-ID"];
682  }
683 
684  if (session.size() == 0)
685  {
686  // No session ID, use IP address instead
687  session = addr.toString().toLatin1();
688  }
689  if (!m_connections.contains(session))
690  {
691  AirplayConnection apcon;
692  m_connections.insert(session, apcon);
693  }
694 
695  if (req->GetURI() == "/reverse")
696  {
697  QTcpSocket *s = m_connections[session].m_reverseSocket;
698  if (s != socket && s != nullptr)
699  {
700  LOG(VB_GENERAL, LOG_ERR, LOC +
701  "Already have a different reverse socket for this connection.");
702  return;
703  }
704  m_connections[session].m_reverseSocket = socket;
706  header = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
707  SendResponse(socket, status, header, content_type, body);
708  return;
709  }
710 
711  QTcpSocket *s = m_connections[session].m_controlSocket;
712  if (s != socket && s != nullptr)
713  {
714  LOG(VB_GENERAL, LOG_ERR, LOC +
715  "Already have a different control socket for this connection.");
716  return;
717  }
718  m_connections[session].m_controlSocket = socket;
719 
720  if (m_connections[session].m_controlSocket != nullptr &&
721  m_connections[session].m_reverseSocket != nullptr &&
722  !m_connections[session].m_initialized)
723  {
724  // Got a full connection, disconnect any other clients
725  DisconnectAllClients(session);
726  m_connections[session].m_initialized = true;
727 
728  MythNotification n(tr("New Connection"), tr("AirPlay"),
729  tr("from %1").arg(socket->peerAddress().toString()));
730  // Don't show it during playback
731  n.SetVisibility(n.GetVisibility() & ~MythNotification::kPlayback);
733  }
734 
735  double position = 0.0F;
736  double duration = 0.0F;
737  float playerspeed = 0.0F;
738  bool playing = false;
739  QString pathname;
740  GetPlayerStatus(playing, playerspeed, position, duration, pathname);
741 
742  if (playing && pathname != m_pathname)
743  {
744  // not ours
745  playing = false;
746  }
747  if (playing && duration > 0.01 && position < 0.01)
748  {
749  // Assume playback hasn't started yet, get saved position
750  position = m_connections[session].m_position;
751  }
752  if (!playing && m_connections[session].m_was_playing)
753  {
754  // playback got interrupted, notify client to stop
755  if (SendReverseEvent(session, AP_EVENT_STOPPED))
756  {
757  m_connections[session].m_was_playing = false;
758  }
759  }
760  else
761  {
762  m_connections[session].m_was_playing = playing;
763  }
764 
765  if (gCoreContext->GetBoolSetting("AirPlayPasswordEnabled", false))
766  {
767  if (m_nonce.isEmpty())
768  {
770  }
771  header = QString("WWW-Authenticate: Digest realm=\"AirPlay\", "
772  "nonce=\"%1\"\r\n").arg(m_nonce).toLatin1();
773  if (!req->GetHeaders().contains("Authorization"))
774  {
776  header, content_type, body);
777  return;
778  }
779 
780  QByteArray auth;
781  if (DigestMd5Response(req->GetHeaders()["Authorization"], req->GetMethod(), m_nonce,
782  gCoreContext->GetSetting("AirPlayPassword"),
783  auth) == auth)
784  {
785  LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay client authenticated");
786  }
787  else
788  {
789  LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay authentication failed");
791  header, content_type, body);
792  return;
793  }
794  header = "";
795  }
796 
797  if (req->GetURI() == "/server-info")
798  {
799  content_type = "text/x-apple-plist+xml\r\n";
800  body = SERVER_INFO;
801  body.replace("%1", GetMacAddress());
802  LOG(VB_GENERAL, LOG_INFO, body);
803  }
804  else if (req->GetURI() == "/scrub")
805  {
806  double pos = req->GetQueryValue("position").toDouble();
807  if (req->GetMethod() == "POST")
808  {
809  // this may be received before playback starts...
810  uint64_t intpos = (uint64_t)pos;
811  m_connections[session].m_position = pos;
812  LOG(VB_GENERAL, LOG_INFO, LOC +
813  QString("Scrub: (post) seek to %1").arg(intpos));
814  SeekPosition(intpos);
815  }
816  else if (req->GetMethod() == "GET")
817  {
818  content_type = "text/parameters\r\n";
819  body = QString("duration: %1\r\nposition: %2\r\n")
820  .arg(duration, 0, 'f', 6, '0')
821  .arg(position, 0, 'f', 6, '0');
822 
823  LOG(VB_GENERAL, LOG_INFO, LOC +
824  QString("Scrub: (get) returned %1 of %2")
825  .arg(position).arg(duration));
826 
827  /*
828  if (playing && playerspeed < 1.0F)
829  {
830  SendReverseEvent(session, AP_EVENT_PLAYING);
831  QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
832  Qt::NoModifier, ACTION_PLAY);
833  qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
834  }
835  */
836  }
837  }
838  else if (req->GetURI() == "/stop")
839  {
840  StopSession(session);
841  }
842  else if (req->GetURI() == "/photo")
843  {
844  if (req->GetMethod() == "PUT")
845  {
846  // this may be received before playback starts...
847  QImage image = QImage::fromData(req->GetBody());
848  bool png =
849  req->GetBody().size() > 3 && req->GetBody()[1] == 'P' &&
850  req->GetBody()[2] == 'N' && req->GetBody()[3] == 'G';
851  LOG(VB_GENERAL, LOG_INFO, LOC +
852  QString("Received %1x%2 %3 photo")
853  .arg(image.width()).arg(image.height()).
854  arg(png ? "jpeg" : "png"));
855 
856  if (m_connections[session].m_notificationid < 0)
857  {
858  m_connections[session].m_notificationid =
860  }
861  // send full screen display notification
863  n.SetId(m_connections[session].m_notificationid);
864  n.SetParent(this);
865  n.SetFullScreen(true);
867  // This is a photo session
868  m_connections[session].m_photos = true;
869  }
870  }
871  else if (req->GetURI() == "/slideshow-features")
872  {
873  LOG(VB_GENERAL, LOG_INFO, LOC +
874  "Slideshow functionality not implemented.");
875  }
876  else if (req->GetURI() == "/authorize")
877  {
878  LOG(VB_GENERAL, LOG_INFO, LOC + "Ignoring authorize request.");
879  }
880  else if (req->GetURI() == "/setProperty")
881  {
882  status = HTTP_STATUS_NOT_FOUND;
883  }
884  else if (req->GetURI() == "/getProperty")
885  {
886  status = HTTP_STATUS_NOT_FOUND;
887  }
888  else if (req->GetURI() == "/rate")
889  {
890  float rate = req->GetQueryValue("value").toFloat();
891  m_connections[session].m_speed = rate;
892 
893  if (rate < 1.0F)
894  {
895  if (playerspeed > 0.0F)
896  {
897  PausePlayback();
898  }
900  }
901  else
902  {
903  if (playerspeed < 1.0F)
904  {
905  UnpausePlayback();
906  }
908  // If there's any photos left displayed, hide them
909  HideAllPhotos();
910  }
911  }
912  else if (req->GetURI() == "/play")
913  {
914  QByteArray file;
915  double start_pos = 0.0F;
916  if (req->GetHeaders().contains("Content-Type") &&
917  req->GetHeaders()["Content-Type"] == "application/x-apple-binary-plist")
918  {
919  PList plist(req->GetBody());
920  LOG(VB_GENERAL, LOG_DEBUG, LOC + plist.ToString());
921 
922  QVariant start = plist.GetValue("Start-Position");
923  QVariant content = plist.GetValue("Content-Location");
924  if (start.isValid() && start.canConvert<double>())
925  start_pos = start.toDouble();
926  if (content.isValid() && content.canConvert<QByteArray>())
927  file = content.toByteArray();
928  }
929  else
930  {
931  QMap<QByteArray,QByteArray> headers = req->GetHeadersFromBody();
932  file = headers["Content-Location"];
933  start_pos = headers["Start-Position"].toDouble();
934  }
935 
936  if (!file.isEmpty())
937  {
938  m_pathname = QUrl::fromPercentEncoding(file);
940  GetPlayerStatus(playing, playerspeed, position, duration, pathname);
941  m_connections[session].m_url = QUrl(m_pathname);
942  m_connections[session].m_position = start_pos * duration;
943  if (TV::IsTVRunning())
944  {
945  HideAllPhotos();
946  }
947  if (duration * start_pos >= .1)
948  {
949  // not point seeking so close to the beginning
950  SeekPosition(duration * start_pos);
951  }
952  }
953 
955  LOG(VB_GENERAL, LOG_INFO, LOC + QString("File: '%1' start_pos '%2'")
956  .arg(file.data()).arg(start_pos));
957  }
958  else if (req->GetURI() == "/playback-info")
959  {
960  content_type = "text/x-apple-plist+xml\r\n";
961 
962  if (!playing)
963  {
964  body = NOT_READY;
966  }
967  else
968  {
969  body = PLAYBACK_INFO;
970  body.replace("%1", QString("%1").arg(duration, 0, 'f', 6, '0'));
971  body.replace("%2", QString("%1").arg(duration, 0, 'f', 6, '0')); // cached
972  body.replace("%3", QString("%1").arg(position, 0, 'f', 6, '0'));
973  body.replace("%4", playerspeed > 0.0F ? "1.0" : "0.0");
974  LOG(VB_GENERAL, LOG_DEBUG, body);
975  SendReverseEvent(session, playerspeed > 0.0F ? AP_EVENT_PLAYING :
977  }
978  }
979  SendResponse(socket, status, header, content_type, body);
980 }
981 
982 void MythAirplayServer::SendResponse(QTcpSocket *socket,
983  int status, const QByteArray& header,
984  const QByteArray& content_type, const QString& body)
985 {
986  if (!socket || !m_incoming.contains(socket) ||
987  socket->state() != QAbstractSocket::ConnectedState)
988  return;
989  QTextStream response(socket);
990  response.setCodec("UTF-8");
991  QByteArray reply;
992  reply.append("HTTP/1.1 ");
993  reply.append(QString::number(status));
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"));
999  reply.append(" GMT\r\n");
1000  if (header.size())
1001  reply.append(header);
1002 
1003  if (body.size())
1004  {
1005  reply.append("Content-Type: ");
1006  reply.append(content_type);
1007  reply.append("Content-Length: ");
1008  reply.append(QString::number(body.size()));
1009  }
1010  else
1011  {
1012  reply.append("Content-Length: 0");
1013  }
1014  reply.append("\r\n\r\n");
1015 
1016  if (body.size())
1017  reply.append(body);
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 
1026 bool MythAirplayServer::SendReverseEvent(QByteArray &session,
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  response.setCodec("UTF-8");
1049  QByteArray reply;
1050  reply.append("POST /event HTTP/1.1\r\n");
1051  reply.append("Content-Type: text/x-apple-plist+xml\r\n");
1052  reply.append("Content-Length: ");
1053  reply.append(QString::number(body.size()));
1054  reply.append("\r\n");
1055  reply.append("x-apple-session-id: ");
1056  reply.append(session);
1057  reply.append("\r\n\r\n");
1058  if (body.size())
1059  reply.append(body);
1060 
1061  response << reply;
1062  response.flush();
1063 
1064  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send reverse: %1 \n\n%2\n")
1065  .arg(m_connections[session].m_reverseSocket->flush())
1066  .arg(reply.data()));
1067  return true;
1068 }
1069 
1071 {
1072  switch (event)
1073  {
1074  case AP_EVENT_PLAYING: return "playing";
1075  case AP_EVENT_PAUSED: return "paused";
1076  case AP_EVENT_LOADING: return "loading";
1077  case AP_EVENT_STOPPED: return "stopped";
1078  case AP_EVENT_NONE: return "none";
1079  default: return "";
1080  }
1081 }
1082 
1083 void MythAirplayServer::GetPlayerStatus(bool &playing, float &speed,
1084  double &position, double &duration,
1085  QString &pathname)
1086 {
1087  QVariantMap state;
1089 
1090  if (state.contains("state"))
1091  playing = state["state"].toString() != "idle";
1092  if (state.contains("playspeed"))
1093  speed = state["playspeed"].toFloat();
1094  if (state.contains("secondsplayed"))
1095  position = state["secondsplayed"].toDouble();
1096  if (state.contains("totalseconds"))
1097  duration = state["totalseconds"].toDouble();
1098  if (state.contains("pathname"))
1099  pathname = state["pathname"].toString();
1100 }
1101 
1103 {
1104  QString id = AirPlayHardwareId();
1105 
1106  QString res;
1107  for (int i = 1; i <= id.size(); i++)
1108  {
1109  res.append(id[i-1]);
1110  if (i % 2 == 0 && i != id.size())
1111  {
1112  res.append(':');
1113  }
1114  }
1115  return res;
1116 }
1117 
1118 void MythAirplayServer::StopSession(const QByteArray &session)
1119 {
1120  AirplayConnection& cnx = m_connections[session];
1121 
1122  if (cnx.m_photos)
1123  {
1124  if (cnx.m_notificationid > 0)
1125  {
1126  // close any photos that could be displayed
1128  cnx.m_notificationid = -1;
1129  }
1130  return;
1131  }
1132  cnx.m_stopped = true;
1133  double position = 0.0F;
1134  double duration = 0.0F;
1135  float playerspeed = 0.0F;
1136  bool playing = false;
1137  QString pathname;
1138  GetPlayerStatus(playing, playerspeed, position, duration, pathname);
1139  if (pathname != m_pathname)
1140  {
1141  // not ours
1142  return;
1143  }
1144  if (!playing)
1145  {
1146  return;
1147  }
1148  StopPlayback();
1149 }
1150 
1151 void MythAirplayServer::DisconnectAllClients(const QByteArray &session)
1152 {
1153  QMutexLocker locker(m_lock);
1154  QHash<QByteArray,AirplayConnection>::iterator it = m_connections.begin();
1155  AirplayConnection& current_cnx = m_connections[session];
1156 
1157  while (it != m_connections.end())
1158  {
1159  QTcpSocket *socket;
1160  AirplayConnection& cnx = it.value();
1161 
1162  if (it.key() == session ||
1163  (current_cnx.m_reverseSocket && cnx.m_reverseSocket &&
1164  current_cnx.m_reverseSocket->peerAddress() == cnx.m_reverseSocket->peerAddress()) ||
1165  (current_cnx.m_controlSocket && cnx.m_controlSocket &&
1166  current_cnx.m_controlSocket->peerAddress() == cnx.m_controlSocket->peerAddress()))
1167  {
1168  // ignore if the connection is the currently active one or
1169  // from the same IP address
1170  ++it;
1171  continue;
1172  }
1173  if (!(*it).m_stopped)
1174  {
1175  StopSession(it.key());
1176  }
1177  socket = cnx.m_reverseSocket;
1178  if (socket)
1179  {
1180  socket->disconnect();
1181  socket->close();
1182  m_sockets.removeOne(socket);
1183  socket->deleteLater();
1184  if (m_incoming.contains(socket))
1185  {
1186  delete m_incoming[socket];
1187  m_incoming.remove(socket);
1188  }
1189  }
1190  socket = cnx.m_controlSocket;
1191  if (socket)
1192  {
1193  socket->disconnect();
1194  socket->close();
1195  m_sockets.removeOne(socket);
1196  socket->deleteLater();
1197  if (m_incoming.contains(socket))
1198  {
1199  delete m_incoming[socket];
1200  m_incoming.remove(socket);
1201  }
1202  }
1203  it = m_connections.erase(it);
1204  }
1205 }
1206 
1207 void MythAirplayServer::StartPlayback(const QString &pathname)
1208 {
1209  if (TV::IsTVRunning())
1210  {
1211  StopPlayback();
1212  }
1213  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1214  QString("Sending ACTION_HANDLEMEDIA for %1")
1215  .arg(pathname));
1217  QStringList(pathname));
1218  qApp->postEvent(GetMythMainWindow(), me);
1219  // Wait until we receive that the play has started
1220  gCoreContext->WaitUntilSignals(SIGNAL(TVPlaybackStarted()),
1221  SIGNAL(TVPlaybackAborted()),
1222  nullptr);
1223  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1224  QString("ACTION_HANDLEMEDIA completed"));
1225 }
1226 
1228 {
1229  if (TV::IsTVRunning())
1230  {
1231  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1232  QString("Sending ACTION_STOP for %1")
1233  .arg(m_pathname));
1234 
1235  QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
1236  Qt::NoModifier, ACTION_STOP);
1237  qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1238  // Wait until we receive that playback has stopped
1239  gCoreContext->WaitUntilSignals(SIGNAL(TVPlaybackStopped()),
1240  SIGNAL(TVPlaybackAborted()),
1241  nullptr);
1242  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1243  QString("ACTION_STOP completed"));
1244  }
1245  else
1246  {
1247  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1248  QString("Playback not running, nothing to stop"));
1249  }
1250 }
1251 
1252 void MythAirplayServer::SeekPosition(uint64_t position)
1253 {
1254  if (TV::IsTVRunning())
1255  {
1256  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1257  QString("Sending ACTION_SEEKABSOLUTE(%1) for %2")
1258  .arg(position)
1259  .arg(m_pathname));
1260 
1262  QStringList(QString::number(position)));
1263  qApp->postEvent(GetMythMainWindow(), me);
1264  // Wait until we receive that the seek has completed
1265  gCoreContext->WaitUntilSignals(SIGNAL(TVPlaybackSought(qint64)),
1266  SIGNAL(TVPlaybackStopped()),
1267  SIGNAL(TVPlaybackAborted()),
1268  nullptr);
1269  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1270  QString("ACTION_SEEKABSOLUTE completed"));
1271  }
1272  else
1273  {
1274  LOG(VB_PLAYBACK, LOG_WARNING, LOC +
1275  QString("Trying to seek when playback hasn't started"));
1276  }
1277 }
1278 
1280 {
1281  if (TV::IsTVRunning() && !TV::IsPaused())
1282  {
1283  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1284  QString("Sending ACTION_PAUSE for %1")
1285  .arg(m_pathname));
1286 
1287  QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
1288  Qt::NoModifier, ACTION_PAUSE);
1289  qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1290  // Wait until we receive that playback has stopped
1291  gCoreContext->WaitUntilSignals(SIGNAL(TVPlaybackPaused()),
1292  SIGNAL(TVPlaybackStopped()),
1293  SIGNAL(TVPlaybackAborted()),
1294  nullptr);
1295  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1296  QString("ACTION_PAUSE completed"));
1297  }
1298  else
1299  {
1300  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1301  QString("Playback not running, nothing to pause"));
1302  }
1303 }
1304 
1306 {
1307  if (TV::IsTVRunning())
1308  {
1309  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1310  QString("Sending ACTION_PLAY for %1")
1311  .arg(m_pathname));
1312 
1313  QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0,
1314  Qt::NoModifier, ACTION_PLAY);
1315  qApp->postEvent(GetMythMainWindow(), (QEvent*)ke);
1316  // Wait until we receive that playback has stopped
1317  gCoreContext->WaitUntilSignals(SIGNAL(TVPlaybackPlaying()),
1318  SIGNAL(TVPlaybackStopped()),
1319  SIGNAL(TVPlaybackAborted()),
1320  nullptr);
1321  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1322  QString("ACTION_PLAY completed"));
1323  }
1324  else
1325  {
1326  LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
1327  QString("Playback not running, nothing to unpause"));
1328  }
1329 }
1330 
1332 {
1333  // playback has started, dismiss any currently displayed photo
1334  QHash<QByteArray,AirplayConnection>::iterator it = m_connections.begin();
1335 
1336  while (it != m_connections.end())
1337  {
1338  AirplayConnection& cnx = it.value();
1339 
1340  if (cnx.m_photos)
1341  {
1342  cnx.UnRegister();
1343  }
1344  ++it;
1345  }
1346 }
AirplayEvent
void start(QThread::Priority=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:294
QMap< QByteArray, QByteArray > GetHeadersFromBody(void)
void SetParent(void *parent)
contains the parent address.
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:46
~APHTTPRequest()=default
void DisconnectAllClients(const QByteArray &session)
QTcpSocket * m_reverseSocket
void HandleResponse(APHTTPRequest *req, QTcpSocket *socket)
QByteArray StatusToString(int status)
#define AIRPLAY_PORT_RANGE
QString toString(MarkTypes type)
static void Cleanup(void)
static QMutex * gMythAirplayServerMutex
void SetId(int id)
Optional MythNotification elements.
#define EVENT_INFO
void SaveSetting(const QString &key, int newValue)
void StartPlayback(const QString &pathname)
static bool IsPaused(void)
Returns true if a TV playback is currently going; otherwise returns false.
Definition: tv_play.cpp:6443
bool wait(unsigned long time=ULONG_MAX)
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:311
QByteArray GetQueryValue(const QByteArray &key)
#define HTTP_STATUS_SWITCHING_PROTOCOLS
void GetPlayerStatus(bool &playing, float &speed, double &position, double &duration, QString &pathname)
QByteArray & GetBody(void)
void UnRegister(void *from, int id, bool closeimemdiately=false)
Unregister the client.
#define HTTP_STATUS_NOT_IMPLEMENTED
bool Register(uint16_t port, const QByteArray &type, const QByteArray &name, const QByteArray &txt)
static bool Create(void)
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GenerateNonce(void)
APHTTPRequest(QByteArray &data)
static float * vals
Definition: tentacle3d.c:16
QString AirPlayHardwareId()
#define ACTION_STOP
Definition: tv_actions.h:8
#define ACTION_HANDLEMEDIA
Definition: mythuiactions.h:21
#define AIRPLAY_HARDWARE_ID_SIZE
A parser for binary property lists, using QVariant for internal storage.
Definition: plist.h:8
static bool IsTVRunning(void)
Definition: tv_play.cpp:242
QTcpSocket * m_controlSocket
This class is used as a container for messages.
Definition: mythevent.h:16
#define HTTP_STATUS_UNAUTHORIZED
void Append(QByteArray &data)
QList< QPair< QByteArray, QByteArray > > m_queries
QMap< QByteArray, QByteArray > m_headers
QByteArray & GetURI(void)
void SendResponse(QTcpSocket *socket, int status, const QByteArray &header, const QByteArray &content_type, const QString &body)
int tryListeningPort(int baseport, int range=1)
tryListeningPort
Definition: serverpool.cpp:680
#define ACTION_PLAY
Definition: tv_actions.h:30
static MThread * gMythAirplayServerThread
#define HTTP_STATUS_NOT_FOUND
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
QString GetSetting(const QString &key, const QString &defaultval="")
bool SendReverseEvent(QByteArray &session, AirplayEvent event)
void exit(int retcode=0)
Use this to exit from the thread if you are using a Qt event loop.
Definition: mthread.cpp:289
bool isRunning(void) const
Definition: mthread.cpp:274
#define ACTION_PAUSE
Definition: tv_actions.h:15
QHash< QByteArray, AirplayConnection > m_connections
bool Queue(const MythNotification &notification)
Queue a notification Queue() is thread-safe and can be called from anywhere.
bool ReAnnounceService(void)
QMap< QByteArray, QByteArray > & GetHeaders(void)
const char * name
Definition: ParseText.cpp:328
~MythAirplayServer(void) override
#define AIRPLAY_SERVER_VERSION_STR
QByteArray DigestMd5Response(const QString &response, const QString &option, const QString &nonce, const QString &password, QByteArray &auth)
MythMainWindow * GetMythMainWindow(void)
int Register(void *from)
An application can register in which case it will be assigned a reusable screen, which can be modifie...
PictureAttribute next(PictureAttributeSupported supported, PictureAttribute attribute)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
static MythAirplayServer * gMythAirplayServer
bool GetBoolSetting(const QString &key, bool defaultval=false)
void newConnection(QTcpSocket *client)
#define NOT_READY
void WaitUntilSignals(const char *signal1,...)
Wait until either of the provided signals have been received.
VNMask GetVisibility(void) const
void SeekPosition(uint64_t position)
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:244
#define LOC
void SetFullScreen(bool f)
a notification may request to be displayed in full screen, this request may not be fullfilled should ...
static long int random(void)
Definition: compat.h:149
void StopSession(const QByteArray &session)
BonjourRegister * m_bonjour
QList< QTcpSocket * > m_sockets
#define SERVER_INFO
QByteArray & GetMethod(void)
#define ACTION_SEEKABSOLUTE
Definition: tv_actions.h:40
#define HTTP_STATUS_OK
#define PLAYBACK_INFO
QString GetHostName(void)
QByteArray GetLine(void)
void SendSystemEvent(const QString &msg)
static void GetFreshState(QVariantMap &state)
QHash< QTcpSocket *, APHTTPRequest * > m_incoming
QString eventToString(AirplayEvent event)
MythNotificationCenter * GetNotificationCenter(void)
void SetVisibility(VNMask n)
define a bitmask of Visibility