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