MythTV  master
mythraopdevice.cpp
Go to the documentation of this file.
1 #include <QTimer>
2 #include <QtEndian>
3 #include <QNetworkInterface>
4 #include <QCoreApplication>
5 #include <QtAlgorithms>
6 
7 #include "mthread.h"
8 #include "mythlogging.h"
9 #include "mythcorecontext.h"
10 #include "mythmainwindow.h"
11 
12 #include "bonjourregister.h"
13 #include "mythraopconnection.h"
14 #include "mythraopdevice.h"
15 #include "mythairplayserver.h"
16 
19 QMutex *MythRAOPDevice::gMythRAOPDeviceMutex = new QMutex(QMutex::Recursive);
20 
21 #define LOC QString("RAOP Device: ")
22 
24 {
25  QMutexLocker locker(gMythRAOPDeviceMutex);
26 
27  // don't bother trying to start if there is no private key
29  {
30  LOG(VB_GENERAL, LOG_ERR, LOC + "Aborting startup - no key found.");
31  return false;
32  }
33 
34  // create the device thread
36  gMythRAOPDeviceThread = new MThread("RAOPDevice");
38  {
39  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create RAOP device thread.");
40  return false;
41  }
42 
43  // create the device object
44  if (!gMythRAOPDevice)
46  if (!gMythRAOPDevice)
47  {
48  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create RAOP device object.");
49  return false;
50  }
51 
52  // start the thread
54  {
56  QObject::connect(
57  gMythRAOPDeviceThread->qthread(), SIGNAL(started()),
58  gMythRAOPDevice, SLOT(Start()));
59  QObject::connect(
60  gMythRAOPDeviceThread->qthread(), SIGNAL(finished()),
61  gMythRAOPDevice, SLOT(Stop()));
62  gMythRAOPDeviceThread->start(QThread::LowestPriority);
63  }
64 
65  LOG(VB_GENERAL, LOG_INFO, LOC + "Created RAOP device objects.");
66  return true;
67 }
68 
70 {
71  LOG(VB_GENERAL, LOG_INFO, LOC + "Cleaning up.");
72 
73  QMutexLocker locker(gMythRAOPDeviceMutex);
75  {
78  }
79  delete gMythRAOPDeviceThread;
80  gMythRAOPDeviceThread = nullptr;
81 
82  delete gMythRAOPDevice;
83  gMythRAOPDevice = nullptr;
84 }
85 
87  : m_lock(new QMutex(QMutex::Recursive))
88 {
89  m_hardwareId = QByteArray::fromHex(AirPlayHardwareId().toLatin1());
90 }
91 
93 {
94  delete m_lock;
95  m_lock = nullptr;
96 }
97 
99 {
100  QMutexLocker locker(m_lock);
101 
102  // invalidate
103  m_valid = false;
104 
105  // disconnect from mDNS
106  delete m_bonjour;
107  m_bonjour = nullptr;
108 
109  // disconnect clients
110  DeleteAllClients(nullptr);
111 }
112 
114 {
115  QMutexLocker locker(m_lock);
116 
117  // already started?
118  if (m_valid)
119  return;
120 
121  // join the dots
122  connect(this, SIGNAL(newConnection(QTcpSocket *)),
123  this, SLOT(newConnection(QTcpSocket *)));
124 
127  // start listening for connections (try a few ports in case the default is in use)
128  if (m_setupPort < 0)
129  {
130  LOG(VB_GENERAL, LOG_ERR, LOC +
131  "Failed to find a port for incoming connections.");
132  }
133  else
134  {
135  LOG(VB_GENERAL, LOG_INFO, LOC +
136  QString("Listening for connections on port %1").arg(m_setupPort));
137 
138  if (!RegisterForBonjour())
139  {
140  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service.");
141  return;
142  }
143  }
144 
145  m_valid = true;
146 }
147 
149 {
150  Teardown();
151 }
152 
154 {
155  // announce service
156  m_bonjour = new BonjourRegister(this);
157 
158  // give each frontend a unique name
159  int multiple = m_setupPort - m_basePort;
160  if (multiple > 0)
161  m_name += QString::number(multiple);
162 
163  QByteArray name = m_hardwareId.toHex();
164  name.append("@");
165  name.append(m_name);
166  name.append(" on ");
167  name.append(gCoreContext->GetHostName());
168  QByteArray type = "_raop._tcp";
169  QByteArray txt;
170  txt.append(6); txt.append("tp=UDP");
171  txt.append(8); txt.append("sm=false");
172  txt.append(8); txt.append("sv=false");
173  txt.append(4); txt.append("ek=1"); //
174  txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA
175  txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac
176  txt.append(4); txt.append("ch=2"); // audio channels
177  txt.append(5); txt.append("ss=16"); // sample size
178  txt.append(8); txt.append("sr=44100"); // sample rate
179  if (gCoreContext->GetBoolSetting("AirPlayPasswordEnabled"))
180  {
181  txt.append(7); txt.append("pw=true");
182  }
183  else
184  {
185  txt.append(8); txt.append("pw=false");
186  }
187  txt.append(4); txt.append("vn=3");
188  txt.append(9); txt.append("txtvers=1"); // TXT record version 1
189  txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress
190  txt.append(8); txt.append("vs=115.2");
191  txt.append(7); txt.append("da=true");
192  txt.append(11); txt.append("am=MythTV,1");
193 
194  LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4")
195  .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt)));
196  return m_bonjour->Register(m_setupPort, type, name, txt);
197 }
198 
200 {
201  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Receiving new playback message"));
202  DeleteAllClients(nullptr);
203 }
204 
205 void MythRAOPDevice::newConnection(QTcpSocket *client)
206 {
207  QMutexLocker locker(m_lock);
208  LOG(VB_GENERAL, LOG_INFO, LOC + QString("New connection from %1:%2")
209  .arg(client->peerAddress().toString()).arg(client->peerPort()));
210 
211  gCoreContext->SendSystemEvent(QString("AIRTUNES_NEW_CONNECTION"));
212  MythNotification n(tr("New Connection"), tr("AirTunes"),
213  tr("from %1:%2").arg(client->peerAddress().toString()).arg(client->peerPort()));
214  // Don't show it during playback
215  n.SetVisibility(n.GetVisibility() & ~MythNotification::kPlayback);
217 
218  auto *obj = new MythRAOPConnection(this, client, m_hardwareId, 6000);
219 
220  if (obj->Init())
221  {
222  m_clients.append(obj);
223  connect(client, SIGNAL(disconnected()), this, SLOT(deleteClient()));
225  return;
226  }
227 
228  LOG(VB_GENERAL, LOG_ERR, LOC +
229  "Failed to initialise client connection - closing.");
230  delete obj;
231  client->disconnectFromHost();
232  delete client;
233 }
234 
236 {
237  LOG(VB_GENERAL, LOG_DEBUG, LOC + "Entering DeleteClient.");
238  QMutexLocker locker(m_lock);
239  QList<MythRAOPConnection *>::iterator it = m_clients.begin();
240 
241  MythNotification n(tr("Client disconnected"), tr("AirTunes"));
242  gCoreContext->SendSystemEvent(QString("AIRTUNES_DELETE_CONNECTION"));
243  // Don't show it during playback
244  n.SetVisibility(n.GetVisibility() & ~MythNotification::kPlayback);
246 
247  while (it != m_clients.end())
248  {
249  if ((*it)->GetSocket()->state() == QTcpSocket::UnconnectedState)
250  {
251  LOG(VB_GENERAL, LOG_INFO, LOC + "Removing client connection.");
252  delete *it;
253  m_clients.erase(it);
254  break;
255  }
256  ++it;
257  }
258  LOG(VB_GENERAL, LOG_DEBUG, LOC + "Exiting DeleteClient.");
259 }
260 
262 {
263  LOG(VB_GENERAL, LOG_DEBUG, LOC + "Entering DeleteAllClients.");
264  QMutexLocker locker(m_lock);
265 
266  QList<MythRAOPConnection*>::iterator it = m_clients.begin();
267 
268  while (it != m_clients.end())
269  {
270  MythRAOPConnection *client = *it;
271  if (client == keep)
272  {
273  ++it;
274  continue;
275  }
276  LOG(VB_GENERAL, LOG_INFO, LOC +
277  QString("Removing client connection %1:%2")
278  .arg(client->GetSocket()->peerAddress().toString())
279  .arg(client->GetSocket()->peerPort()));
280  delete *it;
281  it = m_clients.erase(it);
282  }
283  LOG(VB_GENERAL, LOG_DEBUG, LOC + "Exiting DeleteAllClients.");
284 }
void start(QThread::Priority=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:294
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:46
void TVPlaybackStarting(void)
bool wait(unsigned long time=ULONG_MAX)
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:311
bool RegisterForBonjour(void)
bool Register(uint16_t port, const QByteArray &type, const QByteArray &name, const QByteArray &txt)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
void newConnection(QTcpSocket *client)
static RSA * LoadKey(void)
LoadKey.
static MythRAOPDevice * gMythRAOPDevice
QString AirPlayHardwareId()
BonjourRegister * m_bonjour
int tryListeningPort(int baseport, int range=1)
tryListeningPort
Definition: serverpool.cpp:682
QTcpSocket * GetSocket()
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
bool Queue(const MythNotification &notification)
Queue a notification Queue() is thread-safe and can be called from anywhere.
static void Cleanup(void)
static QMutex * gMythRAOPDeviceMutex
static bool Create(void)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
~MythRAOPDevice(void) override
bool GetBoolSetting(const QString &key, bool defaultval=false)
VNMask GetVisibility(void) const
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 RegisterForPlayback(QObject *sender, const char *method)
Register sender for TVPlaybackAboutToStart signal.
QList< MythRAOPConnection * > m_clients
#define RAOP_PORT_RANGE
static MThread * gMythRAOPDeviceThread
QString GetHostName(void)
void SendSystemEvent(const QString &msg)
void Teardown(void)
QByteArray m_hardwareId
void DeleteAllClients(MythRAOPConnection *keep)
MythNotificationCenter * GetNotificationCenter(void)
void SetVisibility(VNMask n)
define a bitmask of Visibility