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