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