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  QString uri;
98  if (!alternative.isEmpty())
99  uri = alternative;
100  else if (use_control)
101  uri = m_controlUrl.toString();
102  else
103  uri = m_requestUrl.toString();
104  requestHeaders.append(QString("%1 %2 RTSP/1.0").arg(method, uri));
105  requestHeaders.append(QString("User-Agent: MythTV Ceton Recorder"));
106  requestHeaders.append(QString("CSeq: %1").arg(++m_sequenceNumber));
107  if (m_sessionId != "0")
108  requestHeaders.append(QString("Session: %1").arg(m_sessionId));
109  if (headers != nullptr)
110  {
111  for(int i = 0; i < headers->count(); i++)
112  {
113  const QString& header = headers->at(i);
114  requestHeaders.append(header);
115  }
116  }
117  requestHeaders.append(QString("\r\n"));
118  QString request = requestHeaders.join("\r\n");
119 
120 
121  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("write: %1").arg(request));
122  m_socket->write(request.toLatin1());
123 
124  m_responseHeaders.clear();
125  m_responseContent.clear();
126 
127  if (!waitforanswer)
128  return true;
129 
130  static const QRegularExpression kFirstLineRE { "^RTSP/1.0 (\\d+) ([^\r\n]+)" };
131  static const QRegularExpression kHeaderRE { R"(^([^:]+):\s*([^\r\n]+))" };
132  static const QRegularExpression kBlankLineRE { R"(^[\r\n]*$)" };
133 
134  bool firstLine = true;
135  while (true)
136  {
137  if (!m_socket->canReadLine())
138  {
139  bool ready = m_socket->waitForReadyRead(30 * 1000);
140  if (!ready)
141  {
142  LOG(VB_RECORD, LOG_ERR, LOC + "RTSP server did not respond after 30s");
143  return false;
144  }
145  continue;
146  }
147 
148  QString line = m_socket->readLine();
149  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("read: %1").arg(line));
150 
151  QRegularExpressionMatch match;
152  if (firstLine)
153  {
154  match = kFirstLineRE.match(line);
155  if (!match.hasMatch())
156  {
157  m_responseCode = -1;
159  QString("Could not parse first line of response: '%1'")
160  .arg(line);
161  return false;
162  }
163 
164  QStringList parts = match.capturedTexts();
165  m_responseCode = parts.at(1).toInt();
166  m_responseMessage = parts.at(2);
167 
168  if (m_responseCode != 200)
169  {
171  QString("Server couldn't process the request: '%1'")
172  .arg(m_responseMessage);
173  return false;
174  }
175  firstLine = false;
176  continue;
177  }
178 
179  match = kBlankLineRE.match(line);
180  if (match.hasMatch()) break;
181 
182  match = kHeaderRE.match(line);
183  if (!match.hasMatch())
184  {
185  m_responseCode = -1;
186  m_responseMessage = QString("Could not parse response header: '%1'")
187  .arg(line);
188  return false;
189  }
190  QStringList parts = match.capturedTexts();
191  m_responseHeaders.insert(parts.at(1), parts.at(2));
192  }
193 
194  QString cSeq;
195 
196  if (m_responseHeaders.contains("CSeq"))
197  {
198  cSeq = m_responseHeaders["CSeq"];
199  }
200  else
201  {
202  // Handle broken implementation, such as VLC
203  // doesn't respect the case of "CSeq", so find it regardless of the spelling
204  auto it = std::find_if(m_responseHeaders.cbegin(), m_responseHeaders.cend(),
205  [](const QString& key) -> bool
206  {return key.compare("CSeq", Qt::CaseInsensitive) == 0;});
207  if (it != m_responseHeaders.cend())
208  cSeq = it.value();
209  }
210  if (cSeq != QString("%1").arg(m_sequenceNumber))
211  {
212  LOG(VB_RECORD, LOG_WARNING, LOC +
213  QString("Expected CSeq of %1 but got %2")
214  .arg(m_sequenceNumber).arg(cSeq));
215  }
216 
217  m_responseContent.clear();
218  int contentLength = m_responseHeaders.value("Content-Length").toInt();
219  if (contentLength > 0)
220  {
221  m_responseContent.resize(contentLength);
222  char* data = m_responseContent.data();
223  int bytesRead = 0;
224  while (bytesRead < contentLength)
225  {
226  if (m_socket->bytesAvailable() == 0)
227  m_socket->waitForReadyRead();
228 
229  int count = m_socket->read(data+bytesRead, contentLength-bytesRead);
230  if (count == -1)
231  {
232  m_responseCode = -1;
233  m_responseMessage = "Could not read response content";
234  return false;
235  }
236  bytesRead += count;
237  }
238  LOG(VB_RECORD, LOG_DEBUG, LOC +
239  QString("received: %1").arg(m_responseContent.constData()));
240  }
241  return true;
242 }
243 
244 bool CetonRTSP::GetOptions(QStringList &options)
245 {
246  if (ProcessRequest("OPTIONS"))
247  {
248  static const QRegularExpression kSeparatorRE { ",\\s*" };
249  options = m_responseHeaders.value("Public").split(kSeparatorRE);
250  m_canGetParameter = options.contains("GET_PARAMETER");
251 
252  return true;
253  }
254  return false;
255 }
256 
260 QStringList CetonRTSP::splitLines(const QByteArray &lines)
261 {
262  QStringList list;
263  QTextStream stream(lines);
264  QString line;
265 
266  do
267  {
268  line = stream.readLine();
269  if (!line.isNull())
270  {
271  list.append(line);
272  }
273  }
274  while (!line.isNull());
275 
276  return list;
277 }
278 
283 QString CetonRTSP::readParameters(const QString &key, Params &parameters)
284 {
285  QString val;
286 
287  if (!m_responseHeaders.contains(key))
288  {
289  return val;
290  }
291 
292  QStringList header = m_responseHeaders.value(key).split(";");
293 
294  for (int i = 0; i < header.size(); i++)
295  {
296  QString entry = header[i].trimmed();
297 
298  if (i ==0)
299  {
300  val = entry;
301  continue;
302  }
303  QStringList args = entry.split("=");
304 
305  parameters.insert(args[0].trimmed(),
306  args.size() > 1 ? args[1].trimmed() : QString());
307  }
308  return val;
309 }
310 
315 {
316  if (m_responseHeaders.contains("Content-Base"))
317  {
318  return m_responseHeaders["Content-Base"];
319  }
320  if (m_responseHeaders.contains("Content-Location"))
321  {
322  return m_responseHeaders["Content-Location"];
323  }
324  return m_requestUrl;
325 }
326 
328 {
329  QStringList headers;
330 
331  headers.append("Accept: application/sdp");
332 
333  if (!ProcessRequest("DESCRIBE", &headers))
334  return false;
335 
336  // find control url
337  QStringList lines = splitLines(m_responseContent);
338  bool found = false;
339  QUrl base = m_controlUrl = GetBaseUrl();
340 
341  for (const QString& line : std::as_const(lines))
342  {
343  if (line.startsWith("m="))
344  {
345  if (found)
346  {
347  // another new stream, no need to parse further
348  break;
349  }
350  if (!line.startsWith("m=video"))
351  {
352  // not a video stream
353  continue;
354  }
355  QStringList args = line.split(" ");
356  if (args[2] == "RTP/AVP" && args[3] == "33")
357  {
358  found = true;
359  }
360  continue;
361  }
362  if (line.startsWith("c="))
363  {
364  // TODO, connection parameter
365  // assume we will always get a control entry
366  continue;
367  }
368  if (line.startsWith("a=control:"))
369  {
370  // Per RFC: a=control:rtsp://example.com/foo
371  // This attribute may contain either relative and absolute URLs,
372  // following the rules and conventions set out in RFC 1808 [25].
373  QString url = line.mid(10).trimmed();
374  m_controlUrl = url;
375  if (url == "*")
376  {
377  m_controlUrl = base;
378  }
379  else if (m_controlUrl.isRelative())
380  {
381  m_controlUrl = base.resolved(m_controlUrl);
382  }
383  continue;
384  }
385  }
386 
387  if (!found)
388  {
389  LOG(VB_RECORD, LOG_ERR, LOC + "expected content to be type "
390  "\"m=video 0 RTP/AVP 33\" but it appears not to be");
391  m_controlUrl = QUrl();
392  return false;
393  }
394 
395  return true;
396 }
397 
398 bool CetonRTSP::Setup(ushort clientPort1, ushort clientPort2,
399  ushort &rtpPort, ushort &rtcpPort,
400  uint32_t &ssrc)
401 {
402  LOG(VB_GENERAL, LOG_INFO, QString("CetonRTSP: ") +
403  QString("Transport: RTP/AVP;unicast;client_port=%1-%2")
404  .arg(clientPort1).arg(clientPort2));
405 
406  QStringList extraHeaders;
407  extraHeaders.append(
408  QString("Transport: RTP/AVP;unicast;client_port=%1-%2")
409  .arg(clientPort1).arg(clientPort2));
410 
411  if (!ProcessRequest("SETUP", &extraHeaders, true))
412  return false;
413 
414  Params params;
415  QString session = readParameters("Session", params);
416 
417  if (session.isEmpty())
418  {
419  LOG(VB_RECORD, LOG_ERR, LOC +
420  "session id not found in SETUP response");
421  return false;
422  }
423  if (session.size() < 8)
424  {
425  LOG(VB_RECORD, LOG_WARNING, LOC +
426  "invalid session id received");
427  }
428  m_sessionId = session;
429 
430  if (params.contains("timeout"))
431  {
432  m_timeout = std::chrono::seconds(params["timeout"].toInt());
433  }
434 
435  // QString transport = readParameters("Transport", params);
436  if (params.contains("ssrc"))
437  {
438  bool ok = false;
439  ssrc = params["ssrc"].toUInt(&ok, 16);
440  }
441  if (params.contains("server_port"))
442  {
443  QString line = params["server_port"];
444  QStringList val = line.split("-");
445 
446  rtpPort = val[0].toInt();
447  rtcpPort = val.size() > 1 ? val[1].toInt() : 0;
448  }
449 
450  return true;
451 }
452 
453 bool CetonRTSP::Play(void)
454 {
455  bool result = ProcessRequest("PLAY");
456 
457  StartKeepAlive();
458  return result;
459 }
460 
462 {
463  StopKeepAlive();
464 
465  bool result = ProcessRequest("TEARDOWN");
466 
467  QMutexLocker locker(&s_rtspMutex);
468 
469  delete m_socket;
470  m_socket = nullptr;
471 
472  m_sessionId = "0";
473  return result;
474 }
475 
477 {
478  if (m_timer)
479  return;
480  auto timeout = std::max(m_timeout - 5s, 5s);
481  LOG(VB_RECORD, LOG_DEBUG, LOC +
482  QString("Start KeepAlive, every %1s").arg(timeout.count()));
483  m_timer = startTimer(timeout);
484 }
485 
487 {
488  if (m_timer)
489  {
490  killTimer(m_timer);
491  LOG(VB_RECORD, LOG_DEBUG, LOC + "Stop KeepAlive");
492  }
493  m_timer = 0;
494 }
495 
496 void CetonRTSP::timerEvent(QTimerEvent* /*event*/)
497 {
498  LOG(VB_RECORD, LOG_DEBUG, LOC + "Sending KeepAlive");
499  if (m_canGetParameter)
500  {
501  ProcessRequest("GET_PARAMETER", nullptr, false, false);
502  }
503  else
504  {
505  ProcessRequest("OPTIONS", nullptr, false, false, "*");
506  }
507 }
build_compdb.args
args
Definition: build_compdb.py:11
hardwareprofile.smolt.timeout
float timeout
Definition: smolt.py:102
CetonRTSP::~CetonRTSP
~CetonRTSP() override
Definition: cetonrtsp.cpp:42
CetonRTSP::splitLines
static QStringList splitLines(const QByteArray &lines)
splitLines.
Definition: cetonrtsp.cpp:260
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:398
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:496
CetonRTSP::GetBaseUrl
QUrl GetBaseUrl(void)
Return the base URL for the last DESCRIBE answer.
Definition: cetonrtsp.cpp:314
mythlogging.h
CetonRTSP::Play
bool Play(void)
Definition: cetonrtsp.cpp:453
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:461
CetonRTSP::m_requestUrl
QUrl m_requestUrl
Definition: cetonrtsp.h:60
CetonRTSP::GetOptions
bool GetOptions(QStringList &options)
Definition: cetonrtsp.cpp:244
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:283
CetonRTSP::Describe
bool Describe(void)
Definition: cetonrtsp.cpp:327
CetonRTSP::StartKeepAlive
void StartKeepAlive(void)
Definition: cetonrtsp.cpp:476
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:486
culrcscrapers.music163.lyricsScraper.headers
dictionary headers
Definition: lyricsScraper.py:19
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