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
16
17#include "cetonrtsp.h"
18
19
20#define LOC QString("CetonRTSP(%1): ").arg(m_requestUrl.toString())
21
23
24CetonRTSP::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
32CetonRTSP::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{
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
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
260QStringList 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
283QString 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
398bool 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
454{
455 bool result = ProcessRequest("PLAY");
456
458 return result;
459}
460
462{
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
496void CetonRTSP::timerEvent(QTimerEvent* /*event*/)
497{
498 LOG(VB_RECORD, LOG_DEBUG, LOC + "Sending KeepAlive");
500 {
501 ProcessRequest("GET_PARAMETER", nullptr, false, false);
502 }
503 else
504 {
505 ProcessRequest("OPTIONS", nullptr, false, false, "*");
506 }
507}
#define LOC
-*- Mode: c++ -*- CetonRTSP Copyright (c) 2011 Ronald Frazier Distributed as part of MythTV under GPL...
Definition: cetonrtsp.cpp:20
QMap< QString, QString > Params
Definition: cetonrtsp.h:24
CetonRTSP(const QString &ip, uint tuner, ushort port)
Definition: cetonrtsp.cpp:24
QUrl GetBaseUrl(void)
Return the base URL for the last DESCRIBE answer.
Definition: cetonrtsp.cpp:314
int m_timer
Definition: cetonrtsp.h:68
bool Setup(ushort clientPort1, ushort clientPort2, ushort &rtpPort, ushort &rtcpPort, uint32_t &ssrc)
Definition: cetonrtsp.cpp:398
QString m_sessionId
Definition: cetonrtsp.h:59
bool Play(void)
Definition: cetonrtsp.cpp:453
Params m_responseHeaders
Definition: cetonrtsp.h:65
bool Teardown(void)
Definition: cetonrtsp.cpp:461
static QMutex s_rtspMutex
Definition: cetonrtsp.h:71
QByteArray m_responseContent
Definition: cetonrtsp.h:66
QUrl m_controlUrl
Definition: cetonrtsp.h:61
void StopKeepAlive(void)
Definition: cetonrtsp.cpp:486
bool Describe(void)
Definition: cetonrtsp.cpp:327
static QStringList splitLines(const QByteArray &lines)
splitLines.
Definition: cetonrtsp.cpp:260
QString readParameters(const QString &key, Params &parameters)
readParameters.
Definition: cetonrtsp.cpp:283
bool GetOptions(QStringList &options)
Definition: cetonrtsp.cpp:244
QUrl m_requestUrl
Definition: cetonrtsp.h:60
bool ProcessRequest(const QString &method, const QStringList *headers=nullptr, bool use_control=false, bool waitforanswer=true, const QString &alternative=QString())
Definition: cetonrtsp.cpp:47
void timerEvent(QTimerEvent *event) override
Definition: cetonrtsp.cpp:496
QTcpSocket * m_socket
Definition: cetonrtsp.h:57
~CetonRTSP() override
Definition: cetonrtsp.cpp:42
void StartKeepAlive(void)
Definition: cetonrtsp.cpp:476
uint m_sequenceNumber
Definition: cetonrtsp.h:58
std::chrono::seconds m_timeout
Definition: cetonrtsp.h:67
int m_responseCode
Definition: cetonrtsp.h:63
bool m_canGetParameter
Definition: cetonrtsp.h:69
QString m_responseMessage
Definition: cetonrtsp.h:64
unsigned int uint
Definition: freesurround.h:24
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39