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