MythTV  master
cetonrtsp.cpp
Go to the documentation of this file.
1 
7 #include <QStringList>
8 #include <QTcpSocket>
9 #include <QUrl>
10 #include <QVector>
11 
12 // MythTV includes
13 #include "cetonrtsp.h"
14 #include "mythlogging.h"
15 #include "mythsocket.h"
16 
17 
18 #define LOC QString("CetonRTSP(%1): ").arg(m_requestUrl.toString())
19 
21 
22 CetonRTSP::CetonRTSP(const QString &ip, uint tuner, ushort port)
23 {
24  m_requestUrl.setHost(ip);
25  m_requestUrl.setPort(port);
26  m_requestUrl.setScheme("rtsp");
27  m_requestUrl.setPath(QString("cetonmpeg%1").arg(tuner));
28 }
29 
30 CetonRTSP::CetonRTSP(const QUrl &url) :
31  m_requestUrl(url)
32 {
33  if (url.port() < 0)
34  {
35  // default rtsp port
36  m_requestUrl.setPort(554);
37  }
38 }
39 
41 {
42  StopKeepAlive();
43 }
44 
46  const QString &method, const QStringList* headers,
47  bool use_control, bool waitforanswer, const QString &alternative)
48 {
49  QMutexLocker locker(&s_rtspMutex);
50 
51  m_responseHeaders.clear();
52  m_responseContent.clear();
53 
54  // Create socket if socket object has never been created or in non-connected state
55  if (!m_socket || m_socket->state() != QAbstractSocket::ConnectedState)
56  {
57  if (!m_socket)
58  {
59  m_socket = new QTcpSocket();
60  }
61  else
62  {
63  m_socket->close();
64  }
65  m_socket->connectToHost(m_requestUrl.host(), m_requestUrl.port(),
66  QAbstractSocket::ReadWrite);
67  bool ok = m_socket->waitForConnected();
68 
69  if (!ok)
70  {
71  LOG(VB_GENERAL, LOG_ERR, LOC +
72  QString("Could not connect to server %1:%2")
73  .arg(m_requestUrl.host()).arg(m_requestUrl.port()));
74  delete m_socket;
75  m_socket = nullptr;
76  return false;
77  }
78  }
79  else
80  {
81  // empty socket's waiting data just in case
82  m_socket->waitForReadyRead(30);
83  do
84  {
85  QVector<char> trash;
86  uint avail = m_socket->bytesAvailable();
87  trash.resize(std::max((uint)trash.size(), avail));
88  m_socket->read(trash.data(), avail);
89  m_socket->waitForReadyRead(30);
90  }
91  while (m_socket->bytesAvailable() > 0);
92  }
93 
94  QStringList requestHeaders;
95  requestHeaders.append(QString("%1 %2 RTSP/1.0")
96  .arg(method)
97  .arg(alternative.size() ? alternative :
98  (use_control ? m_controlUrl.toString() : m_requestUrl.toString())));
99  requestHeaders.append(QString("User-Agent: MythTV Ceton Recorder"));
100  requestHeaders.append(QString("CSeq: %1").arg(++m_sequenceNumber));
101  if (m_sessionId != "0")
102  requestHeaders.append(QString("Session: %1").arg(m_sessionId));
103  if (headers != nullptr)
104  {
105  for(int i = 0; i < headers->count(); i++)
106  {
107  const QString& header = headers->at(i);
108  requestHeaders.append(header);
109  }
110  }
111  requestHeaders.append(QString("\r\n"));
112  QString request = requestHeaders.join("\r\n");
113 
114 
115  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("write: %1").arg(request));
116  m_socket->write(request.toLatin1());
117 
118  m_responseHeaders.clear();
119  m_responseContent.clear();
120 
121  if (!waitforanswer)
122  return true;
123 
124  QRegExp firstLineRegex(
125  "^RTSP/1.0 (\\d+) ([^\r\n]+)", Qt::CaseSensitive, QRegExp::RegExp2);
126  QRegExp headerRegex(
127  "^([^:]+):\\s*([^\\r\\n]+)", Qt::CaseSensitive, QRegExp::RegExp2);
128  QRegExp blankLineRegex(
129  "^[\\r\\n]*$", Qt::CaseSensitive, QRegExp::RegExp2);
130 
131  bool firstLine = true;
132  while (true)
133  {
134  if (!m_socket->canReadLine())
135  {
136  bool ready = m_socket->waitForReadyRead(30 * 1000);
137  if (!ready)
138  {
139  LOG(VB_RECORD, LOG_ERR, LOC + "RTSP server did not respond after 30s");
140  return false;
141  }
142  continue;
143  }
144 
145  QString line = m_socket->readLine();
146  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("read: %1").arg(line));
147 
148  if (firstLine)
149  {
150  if (firstLineRegex.indexIn(line) == -1)
151  {
152  m_responseCode = -1;
154  QString("Could not parse first line of response: '%1'")
155  .arg(line);
156  return false;
157  }
158 
159  QStringList parts = firstLineRegex.capturedTexts();
160  m_responseCode = parts.at(1).toInt();
161  m_responseMessage = parts.at(2);
162 
163  if (m_responseCode != 200)
164  {
166  QString("Server couldn't process the request: '%1'")
167  .arg(m_responseMessage);
168  return false;
169  }
170  firstLine = false;
171  continue;
172  }
173 
174  if (blankLineRegex.indexIn(line) != -1) break;
175 
176  if (headerRegex.indexIn(line) == -1)
177  {
178  m_responseCode = -1;
179  m_responseMessage = QString("Could not parse response header: '%1'")
180  .arg(line);
181  return false;
182  }
183  QStringList parts = headerRegex.capturedTexts();
184  m_responseHeaders.insert(parts.at(1), parts.at(2));
185  }
186 
187  QString cSeq;
188 
189  if (m_responseHeaders.contains("CSeq"))
190  {
191  cSeq = m_responseHeaders["CSeq"];
192  }
193  else
194  {
195  // Handle broken implementation, such as VLC
196  // doesn't respect the case of "CSeq", so find it regardless of the spelling
197  foreach (QString key, m_responseHeaders.keys())
198  {
199  if (key.compare("CSeq", Qt::CaseInsensitive) == 0)
200  {
201  cSeq = m_responseHeaders.value(key);
202  break;
203  }
204  }
205  }
206  if (cSeq != QString("%1").arg(m_sequenceNumber))
207  {
208  LOG(VB_RECORD, LOG_WARNING, LOC +
209  QString("Expected CSeq of %1 but got %2")
210  .arg(m_sequenceNumber).arg(cSeq));
211  }
212 
213  m_responseContent.clear();
214  int contentLength = m_responseHeaders.value("Content-Length").toInt();
215  if (contentLength > 0)
216  {
217  m_responseContent.resize(contentLength);
218  char* data = m_responseContent.data();
219  int bytesRead = 0;
220  while (bytesRead < contentLength)
221  {
222  if (m_socket->bytesAvailable() == 0)
223  m_socket->waitForReadyRead();
224 
225  int count = m_socket->read(data+bytesRead, contentLength-bytesRead);
226  if (count == -1)
227  {
228  m_responseCode = -1;
229  m_responseMessage = "Could not read response content";
230  return false;
231  }
232  bytesRead += count;
233  }
234  LOG(VB_RECORD, LOG_DEBUG, LOC +
235  QString("received: %1").arg(m_responseContent.constData()));
236  }
237  return true;
238 }
239 
240 bool CetonRTSP::GetOptions(QStringList &options)
241 {
242  if (ProcessRequest("OPTIONS"))
243  {
244  options = m_responseHeaders.value("Public").split(QRegExp(",\\s*"));
245  m_canGetParameter = options.contains("GET_PARAMETER");
246 
247  return true;
248  }
249  return false;
250 }
251 
255 QStringList CetonRTSP::splitLines(const QByteArray &lines)
256 {
257  QStringList list;
258  QTextStream stream(lines);
259  QString line;
260 
261  do
262  {
263  line = stream.readLine();
264  if (!line.isNull())
265  {
266  list.append(line);
267  }
268  }
269  while (!line.isNull());
270 
271  return list;
272 }
273 
278 QString CetonRTSP::readParameters(const QString &key, Params &parameters)
279 {
280  QString val;
281 
282  if (!m_responseHeaders.contains(key))
283  {
284  return val;
285  }
286 
287  QStringList header = m_responseHeaders.value(key).split(";");
288 
289  for (int i = 0; i < header.size(); i++)
290  {
291  QString entry = header[i].trimmed();
292 
293  if (i ==0)
294  {
295  val = entry;
296  continue;
297  }
298  QStringList args = entry.split("=");
299 
300  parameters.insert(args[0].trimmed(),
301  args.size() > 1 ? args[1].trimmed() : QString());
302  }
303  return val;
304 }
305 
310 {
311  if (m_responseHeaders.contains("Content-Base"))
312  {
313  return m_responseHeaders["Content-Base"];
314  }
315  if (m_responseHeaders.contains("Content-Location"))
316  {
317  return m_responseHeaders["Content-Location"];
318  }
319  return m_requestUrl;
320 }
321 
323 {
324  QStringList headers;
325 
326  headers.append("Accept: application/sdp");
327 
328  if (!ProcessRequest("DESCRIBE", &headers))
329  return false;
330 
331  // find control url
332  QStringList lines = splitLines(m_responseContent);
333  bool found = false;
334  QUrl base = m_controlUrl = GetBaseUrl();
335 
336  foreach (QString line, lines)
337  {
338  if (line.startsWith("m="))
339  {
340  if (found)
341  {
342  // another new stream, no need to parse further
343  break;
344  }
345  if (!line.startsWith("m=video"))
346  {
347  // not a video stream
348  continue;
349  }
350  QStringList args = line.split(" ");
351  if (args[2] == "RTP/AVP" && args[3] == "33")
352  {
353  found = true;
354  }
355  continue;
356  }
357  if (line.startsWith("c="))
358  {
359  // TODO, connection parameter
360  // assume we will always get a control entry
361  continue;
362  }
363  if (line.startsWith("a=control:"))
364  {
365  // Per RFC: a=control:rtsp://example.com/foo
366  // This attribute may contain either relative and absolute URLs,
367  // following the rules and conventions set out in RFC 1808 [25].
368  QString url = line.mid(10).trimmed();
369  m_controlUrl = url;
370  if (url == "*")
371  {
372  m_controlUrl = base;
373  }
374  else if (m_controlUrl.isRelative())
375  {
376  m_controlUrl = base.resolved(m_controlUrl);
377  }
378  continue;
379  }
380  }
381 
382  if (!found)
383  {
384  LOG(VB_RECORD, LOG_ERR, LOC + "expected content to be type "
385  "\"m=video 0 RTP/AVP 33\" but it appears not to be");
386  m_controlUrl = QUrl();
387  return false;
388  }
389 
390  return true;
391 }
392 
393 bool CetonRTSP::Setup(ushort clientPort1, ushort clientPort2,
394  ushort &rtpPort, ushort &rtcpPort,
395  uint32_t &ssrc)
396 {
397  LOG(VB_GENERAL, LOG_INFO, QString("CetonRTSP: ") +
398  QString("Transport: RTP/AVP;unicast;client_port=%1-%2")
399  .arg(clientPort1).arg(clientPort2));
400 
401  QStringList extraHeaders;
402  extraHeaders.append(
403  QString("Transport: RTP/AVP;unicast;client_port=%1-%2")
404  .arg(clientPort1).arg(clientPort2));
405 
406  if (!ProcessRequest("SETUP", &extraHeaders, true))
407  return false;
408 
409  Params params;
410  QString session = readParameters("Session", params);
411 
412  if (session.isEmpty())
413  {
414  LOG(VB_RECORD, LOG_ERR, LOC +
415  "session id not found in SETUP response");
416  return false;
417  }
418  if (session.size() < 8)
419  {
420  LOG(VB_RECORD, LOG_WARNING, LOC +
421  "invalid session id received");
422  }
423  m_sessionId = session;
424 
425  if (params.contains("timeout"))
426  {
427  m_timeout = params["timeout"].toInt();
428  }
429 
430  QString transport = readParameters("Transport", params);
431  Q_UNUSED(transport);
432  if (params.contains("ssrc"))
433  {
434  bool ok;
435  ssrc = params["ssrc"].toUInt(&ok, 16);
436  }
437  if (params.contains("server_port"))
438  {
439  QString line = params["server_port"];
440  QStringList val = line.split("-");
441 
442  rtpPort = val[0].toInt();
443  rtcpPort = val.size() > 1 ? val[1].toInt() : 0;
444  }
445 
446  return true;
447 }
448 
449 bool CetonRTSP::Play(void)
450 {
451  bool result = ProcessRequest("PLAY");
452 
453  StartKeepAlive();
454  return result;
455 }
456 
458 {
459  StopKeepAlive();
460 
461  bool result = ProcessRequest("TEARDOWN");
462 
463  QMutexLocker locker(&s_rtspMutex);
464 
465  delete m_socket;
466  m_socket = nullptr;
467 
468  m_sessionId = "0";
469  return result;
470 }
471 
473 {
474  if (m_timer)
475  return;
476  int timeout = std::max(m_timeout - 5, 5);
477  LOG(VB_RECORD, LOG_DEBUG, LOC +
478  QString("Start KeepAlive, every %1s").arg(timeout));
479  m_timer = startTimer(timeout * 1000);
480 }
481 
483 {
484  if (m_timer)
485  {
486  killTimer(m_timer);
487  LOG(VB_RECORD, LOG_DEBUG, LOC + "Stop KeepAlive");
488  }
489  m_timer = 0;
490 }
491 
492 void CetonRTSP::timerEvent(QTimerEvent* /*event*/)
493 {
494  QStringList dummy;
495 
496  LOG(VB_RECORD, LOG_DEBUG, LOC + "Sending KeepAlive");
497  if (m_canGetParameter)
498  {
499  ProcessRequest("GET_PARAMETER", nullptr, false, false);
500  }
501  else
502  {
503  ProcessRequest("OPTIONS", nullptr, false, false, "*");
504  }
505 }
QTcpSocket * m_socket
Definition: cetonrtsp.h:55
QUrl GetBaseUrl(void)
Return the base URL for the last DESCRIBE answer.
Definition: cetonrtsp.cpp:309
QString readParameters(const QString &key, Params &parameters)
readParameters.
Definition: cetonrtsp.cpp:278
bool Setup(ushort clientPort1, ushort clientPort2, ushort &rtpPort, ushort &rtcpPort, uint32_t &ssrc)
Definition: cetonrtsp.cpp:393
QUrl m_controlUrl
Definition: cetonrtsp.h:59
QString m_responseMessage
Definition: cetonrtsp.h:62
uint m_sequenceNumber
Definition: cetonrtsp.h:56
void timerEvent(QTimerEvent *) override
Definition: cetonrtsp.cpp:492
bool Describe(void)
Definition: cetonrtsp.cpp:322
static QStringList splitLines(const QByteArray &lines)
splitLines.
Definition: cetonrtsp.cpp:255
int m_responseCode
Definition: cetonrtsp.h:61
bool Play(void)
Definition: cetonrtsp.cpp:449
int m_timeout
Definition: cetonrtsp.h:65
Params m_responseHeaders
Definition: cetonrtsp.h:63
void StopKeepAlive(void)
Definition: cetonrtsp.cpp:482
QUrl m_requestUrl
Definition: cetonrtsp.h:58
bool GetOptions(QStringList &options)
Definition: cetonrtsp.cpp:240
CetonRTSP(const QString &ip, uint tuner, ushort port)
Definition: cetonrtsp.cpp:22
unsigned int uint
Definition: compat.h:140
QByteArray m_responseContent
Definition: cetonrtsp.h:64
bool Teardown(void)
Definition: cetonrtsp.cpp:457
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
bool ProcessRequest(const QString &method, const QStringList *headers=nullptr, bool use_control=false, bool waitforanswer=true, const QString &alternative=QString())
Definition: cetonrtsp.cpp:45
int m_timer
Definition: cetonrtsp.h:66
#define LOC
-*- Mode: c++ -*- CetonRTSP Copyright (c) 2011 Ronald Frazier Distributed as part of MythTV under GPL...
Definition: cetonrtsp.cpp:18
QMap< QString, QString > Params
Definition: cetonrtsp.h:22
void StartKeepAlive(void)
Definition: cetonrtsp.cpp:472
static QMutex s_rtspMutex
Definition: cetonrtsp.h:69
bool m_canGetParameter
Definition: cetonrtsp.h:67
QString m_sessionId
Definition: cetonrtsp.h:57