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