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 send_status("ERR:End of file");
253 else
254 {
255 send_status("OK");
256 emit SendBytes();
257 }
258 }
259 }
260#if 0
261 else if (cmd.startsWith("XON"))
262 {
263 // Used when FlowControl is XON/XOFF
264 }
265 else if (cmd.startsWith("XOFF"))
266 {
267 // Used when FlowControl is XON/XOFF
268 }
269 else if (cmd.startsWith("TuneChannel"))
270 {
271 // Used if we announce that we have a 'tuner'
272
276 }
277#endif
278 else if (cmd.startsWith("IsOpen?"))
279 {
280 if (m_streamer->IsOpen())
281 send_status("OK:Open");
282 else
283 send_status(QString("ERR:Not Open: '%2'")
284 .arg(m_streamer->ErrorString()));
285 }
286 else if (cmd.startsWith("CloseRecorder"))
287 {
288 send_status("OK:Terminating");
289 emit CloseFile();
290 return false;
291 }
292 else if (cmd.startsWith("FlowControl?"))
293 {
294 send_status("OK:Polling");
295 }
296 else if (cmd.startsWith("BlockSize"))
297 {
298 m_streamer->BlockSize(cmd.mid(10).toInt());
299 send_status("OK");
300 }
301 else if (cmd.startsWith("StartStreaming"))
302 {
303 send_status("OK:Started");
304 }
305 else if (cmd.startsWith("StopStreaming"))
306 {
307 /* This does not close the stream! When Myth is done with
308 * this 'recording' ExternalChannel::EnterPowerSavingMode()
309 * will be called, which invokes CloseRecorder() */
310 send_status("OK:Stopped");
311 }
312 else
313 {
314 send_status(QString("ERR:Unknown command '%1'").arg(cmd));
315 LOG(VB_RECORD, LOG_ERR, LOC + QString("Unknown command '%1'")
316 .arg(cmd));
317 }
318
319 return true;
320}
321
322bool Commands::Run(const QString & filename, int data_rate, bool loopinput)
323{
324 QString cmd;
325
326 int poll_cnt = 1;
327 std::array<struct pollfd,2> polls {};
328
329 polls[0].fd = 0;
330 polls[0].events = POLLIN | POLLPRI;
331 polls[0].revents = 0;
332
334
335 m_streamer = new Streamer(this, m_fileName, data_rate, loopinput);
336 auto *streamThread = new QThread(this);
337
338 m_streamer->moveToThread(streamThread);
339 connect(streamThread, &QThread::finished,
340 m_streamer, &QObject::deleteLater);
341
342 connect(this, &Commands::SendBytes,
344
345 streamThread->start();
346
347 QFile input;
348 if (!input.open(stdin, QIODevice::ReadOnly))
349 {
350 LOG(VB_RECORD, LOG_ERR, LOC + "Opening of stdin failed");
351 return false;
352 }
353
354 QTextStream qtin(&input);
355
356 LOG(VB_RECORD, LOG_INFO, LOC + "Listening for commands");
357
358 while (m_run)
359 {
360 int ret = poll(polls.data(), poll_cnt, m_timeout);
361
362 if (polls[0].revents & POLLHUP)
363 {
364 LOG(VB_RECORD, LOG_ERR, LOC + "poll eof (POLLHUP)");
365 break;
366 }
367 if (polls[0].revents & POLLNVAL)
368 {
369 LOG(VB_RECORD, LOG_ERR, LOC + "poll error");
370 return false;
371 }
372
373 if (polls[0].revents & POLLIN)
374 {
375 if (ret > 0)
376 {
377 cmd = qtin.readLine();
378 if (!process_command(cmd))
379 {
380 streamThread->quit();
381 streamThread->wait();
382 delete streamThread;
383 streamThread = nullptr;
384 m_streamer = nullptr;
385 m_run = false;
386 }
387 }
388 else if (ret < 0)
389 {
390 if ((EOVERFLOW == errno))
391 {
392 LOG(VB_RECORD, LOG_ERR, LOC + "command overflow.");
393 break; // we have an error to handle
394 }
395
396 if ((EAGAIN == errno) || (EINTR == errno))
397 {
398 LOG(VB_RECORD, LOG_ERR, LOC + "retry command read.");
399 continue; // errors that tell you to try again
400 }
401
402 LOG(VB_RECORD, LOG_ERR, LOC + "unknown error reading command.");
403 }
404 }
405 }
406
407 return true;
408}
409
410
411int main(int argc, char *argv[])
412{
414
415 if (!cmdline.Parse(argc, argv))
416 {
419 }
420
421 if (cmdline.toBool("showhelp"))
422 {
424 return GENERIC_EXIT_OK;
425 }
426
427 if (cmdline.toBool("showversion"))
428 {
430 return GENERIC_EXIT_OK;
431 }
432
433 bool loopinput = !cmdline.toBool("noloop");
434 int data_rate = cmdline.toInt("data_rate");
435
436 QCoreApplication a(argc, argv);
437 QCoreApplication::setApplicationName("mythfilerecorder");
438
439 int retval = cmdline.ConfigureLogging();
440 if (retval != GENERIC_EXIT_OK)
441 return retval;
442
443 QString filename = "";
444 if (!cmdline.toString("infile").isEmpty())
445 filename = cmdline.toString("infile");
446 else if (!cmdline.GetArgs().empty())
447 filename = cmdline.GetArgs().at(0);
448
450 recorder.Run(filename, data_rate, loopinput);
451
452 return GENERIC_EXIT_OK;
453}
454
455/* 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
STL namespace.