MythTV master
mythfilerecorder.cpp
Go to the documentation of this file.
1
2// C/C++
3#include <algorithm>
4#include <array>
5#include <fcntl.h>
6#include <iostream>
7#include <sys/poll.h>
8#include <sys/stat.h>
9#include <termios.h>
10
11// Qt
12#include <QCoreApplication>
13#include <QDir>
14#include <QFile>
15#include <QTextStream>
16#include <QThread>
17#include <QTime>
18
19// MythTV
20#include "libmyth/mythcontext.h"
24#include "libmythbase/mythversion.h"
25
26// MythFileRecorder
27#include "mythfilerecorder.h"
29
30static constexpr int API_VERSION { 1 };
31static constexpr const char* VERSION { "1.0.0" };
32#define LOC QString("File(%1): ").arg(m_fileName)
33
34Streamer::Streamer(Commands *parent, QString fname,
35 int data_rate, bool loopinput) :
36 m_parent(parent), m_fileName(std::move(fname)),
37 m_loop(loopinput), m_dataRate(data_rate)
38{
39 setObjectName("Streamer");
40 OpenFile();
41 LOG(VB_RECORD, LOG_INFO, LOC + QString("Data Rate: %1").arg(m_dataRate));
42}
43
45{
46 LOG(VB_RECORD, LOG_INFO, LOC + "Streamer::dtor -- begin");
47 CloseFile();
48 LOG(VB_RECORD, LOG_INFO, LOC + "Streamer::dtor -- end");
49}
50
52{
53 m_file = new QFile(m_fileName);
54 if (!m_file || !m_file->open(QIODevice::ReadOnly))
55 {
56 LOG(VB_RECORD, LOG_ERR, LOC + QString("Failed to open '%1' - ")
57 .arg(m_fileName) + ENO);
58 }
59}
60
62{
63 LOG(VB_RECORD, LOG_INFO, LOC + "Streamer::Close -- begin");
64
65 if (m_file)
66 {
67 delete m_file;
68 m_file = nullptr;
69 }
70
71 LOG(VB_RECORD, LOG_INFO, LOC + "Streamer::Close -- end");
72}
73
75{
76 int pkt_size = 0;
77 int buf_size = 0;
78
79 LOG(VB_RECORD, LOG_DEBUG, LOC + "SendBytes -- start");
80
81 if (!m_file)
82 {
83 LOG(VB_GENERAL, LOG_ERR, LOC + "SendBytes -- file not open");
84 return;
85 }
86
87 if (m_file->atEnd())
88 {
89 if (m_loop)
90 m_file->reset();
91 else if (m_buffer.isEmpty())
93 }
94
95 if (!m_file->atEnd())
96 {
97 int read_sz = m_blockSize.loadAcquire();
98 if (!m_startTime.isValid())
100 qint64 delta = m_startTime.secsTo(MythDate::current()) + 1;
101 int rate = (delta * m_dataRate) - m_dataRead;
102
103 read_sz = std::min(rate, read_sz);
104#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
105 read_sz = std::min((m_bufferMax - m_buffer.size()), read_sz);
106#else
107 read_sz = std::min(static_cast<int>(m_bufferMax - m_buffer.size()), read_sz);
108#endif
109
110 if (read_sz > 0)
111 {
112 QByteArray buffer = m_file->read(read_sz);
113
114 pkt_size = buffer.size();
115 if (pkt_size > 0)
116 {
117 m_dataRead += pkt_size;
118 if (m_buffer.size() + pkt_size > m_bufferMax)
119 {
120 // This should never happen
121 LOG(VB_RECORD, LOG_WARNING, LOC +
122 QString("SendBytes -- Overflow: %1 > %2, "
123 "dropping first %3 bytes.")
124 .arg(m_buffer.size() + pkt_size)
125 .arg(m_bufferMax).arg(pkt_size));
126 m_buffer.remove(0, pkt_size);
127 }
128 m_buffer += buffer;
129 }
130 }
131 }
132 if (m_buffer.isEmpty())
133 {
134 LOG(VB_RECORD, LOG_DEBUG, LOC + "SendBytes -- Buffer is empty.");
135 return;
136 }
137
138 buf_size = m_buffer.size();
139 LOG(VB_RECORD, LOG_DEBUG, LOC +
140 QString("SendBytes -- Read %1 from file. %2 bytes buffered")
141 .arg(pkt_size).arg(buf_size));
142
143 int write_len = m_blockSize.loadAcquire();
144 write_len = std::min(write_len, buf_size);
145 LOG(VB_RECORD, LOG_DEBUG, LOC +
146 QString("SendBytes -- writing %1 bytes").arg(write_len));
147
148 int wrote = write(1, m_buffer.constData(), write_len);
149
150 LOG(VB_RECORD, LOG_DEBUG, LOC +
151 QString("SendBytes -- wrote %1 bytes").arg(wrote));
152
153 if (wrote < buf_size)
154 {
155 m_buffer.remove(0, wrote);
156 LOG(VB_RECORD, LOG_WARNING, LOC +
157 QString("%1 bytes unwritten").arg(m_buffer.size()));
158 }
159 else
160 {
161 m_buffer.clear();
162 }
163
164 LOG(VB_RECORD, LOG_DEBUG, LOC + "SendBytes -- end");
165}
166
167
169{
170 setObjectName("Command");
171}
172
173bool Commands::send_status(const QString & status) const
174{
175 QByteArray buf = status.toLatin1() + '\n';
176 int len = write(2, buf.constData(), buf.size());
177 if (len != buf.size())
178 {
179 LOG(VB_RECORD, LOG_ERR, LOC +
180 QString("Status -- Wrote %1 of %2 bytes of status '%3'")
181 .arg(len).arg(status.size()).arg(status));
182 return false;
183 }
184 LOG(VB_RECORD, LOG_DEBUG, "Status: " + status);
185 return true;
186}
187
188bool Commands::process_command(QString & cmd)
189{
190 LOG(VB_RECORD, LOG_DEBUG, LOC + cmd);
191
192 if (cmd.startsWith("Version?"))
193 {
194 send_status(QString("OK:%1").arg(VERSION));
195 return true;
196 }
197 if (cmd.startsWith("APIVersion?"))
198 {
199 send_status(QString("OK:%1").arg(API_VERSION));
200 return true;
201 }
202 if (cmd.startsWith("APIVersion:1"))
203 {
204 QString reply = (API_VERSION == 1) ? "OK:Yes": "OK:No";
205 send_status(reply);
206 return true;
207 }
208 if (cmd.startsWith("HasLock?"))
209 {
210 send_status(m_streamer->IsOpen() ? "OK:Yes" : "OK:No");
211 return true;
212 }
213 if (cmd.startsWith("SignalStrengthPercent"))
214 {
215 send_status(m_streamer->IsOpen() ? "OK:100" : "OK:0");
216 return true;
217 }
218 if (cmd.startsWith("LockTimeout"))
219 {
220 send_status("OK:1000");
221 return true;
222 }
223 if (cmd.startsWith("HasTuner?"))
224 {
225 send_status("OK:No");
226 return true;
227 }
228 if (cmd.startsWith("HasPictureAttributes?"))
229 {
230 send_status("OK:No");
231 return true;
232 }
233
234 if (!m_streamer)
235 {
236 send_status("ERR:Not Initialized");
237 LOG(VB_RECORD, LOG_ERR, LOC + QString("%1 failed - not initialized!")
238 .arg(cmd));
239 return false;
240 }
241
242 if (cmd.startsWith("SendBytes"))
243 {
244 if (!m_streamer->IsOpen())
245 {
246 send_status("ERR:file not open");
247 LOG(VB_RECORD, LOG_ERR, LOC + "SendBytes - file not open.");
248 }
249 else
250 {
251 if (m_eof.loadAcquire() != 0)
252 {
253 send_status("ERR:End of file");
254 }
255 else
256 {
257 send_status("OK");
258 emit SendBytes();
259 }
260 }
261 }
262#if 0
263 else if (cmd.startsWith("XON"))
264 {
265 // Used when FlowControl is XON/XOFF
266 }
267 else if (cmd.startsWith("XOFF"))
268 {
269 // Used when FlowControl is XON/XOFF
270 }
271 else if (cmd.startsWith("TuneChannel"))
272 {
273 // Used if we announce that we have a 'tuner'
274
278 }
279#endif
280 else if (cmd.startsWith("IsOpen?"))
281 {
282 if (m_streamer->IsOpen())
283 send_status("OK:Open");
284 else
285 send_status(QString("ERR:Not Open: '%2'")
286 .arg(m_streamer->ErrorString()));
287 }
288 else if (cmd.startsWith("CloseRecorder"))
289 {
290 send_status("OK:Terminating");
291 emit CloseFile();
292 return false;
293 }
294 else if (cmd.startsWith("FlowControl?"))
295 {
296 send_status("OK:Polling");
297 }
298 else if (cmd.startsWith("BlockSize"))
299 {
300#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
301 m_streamer->BlockSize(cmd.midRef(10).toInt());
302#else
303 m_streamer->BlockSize(QStringView(cmd).mid(10).toInt());
304#endif
305 send_status("OK");
306 }
307 else if (cmd.startsWith("StartStreaming"))
308 {
309 send_status("OK:Started");
310 }
311 else if (cmd.startsWith("StopStreaming"))
312 {
313 /* This does not close the stream! When Myth is done with
314 * this 'recording' ExternalChannel::EnterPowerSavingMode()
315 * will be called, which invokes CloseRecorder() */
316 send_status("OK:Stopped");
317 }
318 else
319 {
320 send_status(QString("ERR:Unknown command '%1'").arg(cmd));
321 LOG(VB_RECORD, LOG_ERR, LOC + QString("Unknown command '%1'")
322 .arg(cmd));
323 }
324
325 return true;
326}
327
328bool Commands::Run(const QString & filename, int data_rate, bool loopinput)
329{
330 QString cmd;
331
332 int poll_cnt = 1;
333 std::array<struct pollfd,2> polls {};
334
335 polls[0].fd = 0;
336 polls[0].events = POLLIN | POLLPRI;
337 polls[0].revents = 0;
338
340
341 m_streamer = new Streamer(this, m_fileName, data_rate, loopinput);
342 auto *streamThread = new QThread(this);
343
344 m_streamer->moveToThread(streamThread);
345 connect(streamThread, &QThread::finished,
346 m_streamer, &QObject::deleteLater);
347
348 connect(this, &Commands::SendBytes,
350
351 streamThread->start();
352
353 QFile input;
354 if (!input.open(stdin, QIODevice::ReadOnly))
355 {
356 LOG(VB_RECORD, LOG_ERR, LOC + "Opening of stdin failed");
357 return false;
358 }
359
360 QTextStream qtin(&input);
361
362 LOG(VB_RECORD, LOG_INFO, LOC + "Listening for commands");
363
364 while (m_run)
365 {
366 int ret = poll(polls.data(), poll_cnt, m_timeout);
367
368 if (polls[0].revents & POLLHUP)
369 {
370 LOG(VB_RECORD, LOG_ERR, LOC + "poll eof (POLLHUP)");
371 break;
372 }
373 if (polls[0].revents & POLLNVAL)
374 {
375 LOG(VB_RECORD, LOG_ERR, LOC + "poll error");
376 return false;
377 }
378
379 if (polls[0].revents & POLLIN)
380 {
381 if (ret > 0)
382 {
383 cmd = qtin.readLine();
384 if (!process_command(cmd))
385 {
386 streamThread->quit();
387 streamThread->wait();
388 delete streamThread;
389 streamThread = nullptr;
390 m_streamer = nullptr;
391 m_run = false;
392 }
393 }
394 else if (ret < 0)
395 {
396 if ((EOVERFLOW == errno))
397 {
398 LOG(VB_RECORD, LOG_ERR, LOC + "command overflow.");
399 break; // we have an error to handle
400 }
401
402 if ((EAGAIN == errno) || (EINTR == errno))
403 {
404 LOG(VB_RECORD, LOG_ERR, LOC + "retry command read.");
405 continue; // errors that tell you to try again
406 }
407
408 LOG(VB_RECORD, LOG_ERR, LOC + "unknown error reading command.");
409 }
410 }
411 }
412
413 return true;
414}
415
416
417int main(int argc, char *argv[])
418{
420
421 if (!cmdline.Parse(argc, argv))
422 {
425 }
426
427 if (cmdline.toBool("showhelp"))
428 {
430 return GENERIC_EXIT_OK;
431 }
432
433 if (cmdline.toBool("showversion"))
434 {
436 return GENERIC_EXIT_OK;
437 }
438
439 bool loopinput = !cmdline.toBool("noloop");
440 int data_rate = cmdline.toInt("data_rate");
441
442 QCoreApplication a(argc, argv);
443 QCoreApplication::setApplicationName("mythfilerecorder");
444
445 int retval = cmdline.ConfigureLogging();
446 if (retval != GENERIC_EXIT_OK)
447 return retval;
448
449 QString filename = "";
450 if (!cmdline.toString("infile").isEmpty())
451 filename = cmdline.toString("infile");
452 else if (!cmdline.GetArgs().empty())
453 filename = cmdline.GetArgs().at(0);
454
456 recorder.Run(filename, data_rate, loopinput);
457
458 return GENERIC_EXIT_OK;
459}
460
461#include "moc_mythfilerecorder.cpp"
Streamer * m_streamer
bool process_command(QString &cmd)
void SendBytes(void)
void CloseFile(void)
QAtomicInt m_eof
bool send_status(const QString &status) const
QString m_fileName
void Run(void)
void setEoF(void)
bool toBool(const QString &key) const
Returns stored QVariant as a boolean.
int toInt(const QString &key) const
Returns stored QVariant as an integer, falling to default if not provided.
virtual bool Parse(int argc, const char *const *argv)
Loop through argv and populate arguments with values.
int ConfigureLogging(const QString &mask="general", bool progress=false)
Read in logging options and initialize the logging interface.
QString toString(const QString &key) const
Returns stored QVariant as a QString, falling to default if not provided.
static void PrintVersion(void)
Print application version information.
QStringList GetArgs(void) const
Return list of additional values provided on the command line independent of any keyword.
void PrintHelp(void) const
Print command line option help.
void SendBytes(void)
QByteArray m_buffer
Commands * m_parent
void BlockSize(int val)
QDateTime m_startTime
void CloseFile(void)
QString m_fileName
QFile * m_file
void OpenFile(void)
~Streamer(void) override
QString ErrorString(void) const
quint64 m_dataRead
QAtomicInt m_blockSize
bool IsOpen(void) const
Streamer(Commands *parent, QString fname, int data_rate, bool loopinput)
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
@ GENERIC_EXIT_INVALID_CMDLINE
Command line parse error.
Definition: exitcodes.h:18
int main(int argc, char *argv[])
#define LOC
static constexpr int API_VERSION
static constexpr const char * VERSION
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:74
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
MythCommFlagCommandLineParser cmdline
def write(text, progress=True)
Definition: mythburn.py:306