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 m_streamer->BlockSize(cmd.mid(10).toInt());
301 send_status("OK");
302 }
303 else if (cmd.startsWith("StartStreaming"))
304 {
305 send_status("OK:Started");
306 }
307 else if (cmd.startsWith("StopStreaming"))
308 {
309 /* This does not close the stream! When Myth is done with
310 * this 'recording' ExternalChannel::EnterPowerSavingMode()
311 * will be called, which invokes CloseRecorder() */
312 send_status("OK:Stopped");
313 }
314 else
315 {
316 send_status(QString("ERR:Unknown command '%1'").arg(cmd));
317 LOG(VB_RECORD, LOG_ERR, LOC + QString("Unknown command '%1'")
318 .arg(cmd));
319 }
320
321 return true;
322}
323
324bool Commands::Run(const QString & filename, int data_rate, bool loopinput)
325{
326 QString cmd;
327
328 int poll_cnt = 1;
329 std::array<struct pollfd,2> polls {};
330
331 polls[0].fd = 0;
332 polls[0].events = POLLIN | POLLPRI;
333 polls[0].revents = 0;
334
336
337 m_streamer = new Streamer(this, m_fileName, data_rate, loopinput);
338 auto *streamThread = new QThread(this);
339
340 m_streamer->moveToThread(streamThread);
341 connect(streamThread, &QThread::finished,
342 m_streamer, &QObject::deleteLater);
343
344 connect(this, &Commands::SendBytes,
346
347 streamThread->start();
348
349 QFile input;
350 if (!input.open(stdin, QIODevice::ReadOnly))
351 {
352 LOG(VB_RECORD, LOG_ERR, LOC + "Opening of stdin failed");
353 return false;
354 }
355
356 QTextStream qtin(&input);
357
358 LOG(VB_RECORD, LOG_INFO, LOC + "Listening for commands");
359
360 while (m_run)
361 {
362 int ret = poll(polls.data(), poll_cnt, m_timeout);
363
364 if (polls[0].revents & POLLHUP)
365 {
366 LOG(VB_RECORD, LOG_ERR, LOC + "poll eof (POLLHUP)");
367 break;
368 }
369 if (polls[0].revents & POLLNVAL)
370 {
371 LOG(VB_RECORD, LOG_ERR, LOC + "poll error");
372 return false;
373 }
374
375 if (polls[0].revents & POLLIN)
376 {
377 if (ret > 0)
378 {
379 cmd = qtin.readLine();
380 if (!process_command(cmd))
381 {
382 streamThread->quit();
383 streamThread->wait();
384 delete streamThread;
385 streamThread = nullptr;
386 m_streamer = nullptr;
387 m_run = false;
388 }
389 }
390 else if (ret < 0)
391 {
392 if ((EOVERFLOW == errno))
393 {
394 LOG(VB_RECORD, LOG_ERR, LOC + "command overflow.");
395 break; // we have an error to handle
396 }
397
398 if ((EAGAIN == errno) || (EINTR == errno))
399 {
400 LOG(VB_RECORD, LOG_ERR, LOC + "retry command read.");
401 continue; // errors that tell you to try again
402 }
403
404 LOG(VB_RECORD, LOG_ERR, LOC + "unknown error reading command.");
405 }
406 }
407 }
408
409 return true;
410}
411
412
413int main(int argc, char *argv[])
414{
416
417 if (!cmdline.Parse(argc, argv))
418 {
421 }
422
423 if (cmdline.toBool("showhelp"))
424 {
426 return GENERIC_EXIT_OK;
427 }
428
429 if (cmdline.toBool("showversion"))
430 {
432 return GENERIC_EXIT_OK;
433 }
434
435 bool loopinput = !cmdline.toBool("noloop");
436 int data_rate = cmdline.toInt("data_rate");
437
438 QCoreApplication a(argc, argv);
439 QCoreApplication::setApplicationName("mythfilerecorder");
440
441 int retval = cmdline.ConfigureLogging();
442 if (retval != GENERIC_EXIT_OK)
443 return retval;
444
445 QString filename = "";
446 if (!cmdline.toString("infile").isEmpty())
447 filename = cmdline.toString("infile");
448 else if (!cmdline.GetArgs().empty())
449 filename = cmdline.GetArgs().at(0);
450
452 recorder.Run(filename, data_rate, loopinput);
453
454 return GENERIC_EXIT_OK;
455}
456
457/* vim: set expandtab tabstop=4 shiftwidth=4: */
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