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 input.open(stdin, QIODevice::ReadOnly);
349 QTextStream qtin(&input);
350
351 LOG(VB_RECORD, LOG_INFO, LOC + "Listening for commands");
352
353 while (m_run)
354 {
355 int ret = poll(polls.data(), poll_cnt, m_timeout);
356
357 if (polls[0].revents & POLLHUP)
358 {
359 LOG(VB_RECORD, LOG_ERR, LOC + "poll eof (POLLHUP)");
360 break;
361 }
362 if (polls[0].revents & POLLNVAL)
363 {
364 LOG(VB_RECORD, LOG_ERR, LOC + "poll error");
365 return false;
366 }
367
368 if (polls[0].revents & POLLIN)
369 {
370 if (ret > 0)
371 {
372 cmd = qtin.readLine();
373 if (!process_command(cmd))
374 {
375 streamThread->quit();
376 streamThread->wait();
377 delete streamThread;
378 streamThread = nullptr;
379 m_streamer = nullptr;
380 m_run = false;
381 }
382 }
383 else if (ret < 0)
384 {
385 if ((EOVERFLOW == errno))
386 {
387 LOG(VB_RECORD, LOG_ERR, LOC + "command overflow.");
388 break; // we have an error to handle
389 }
390
391 if ((EAGAIN == errno) || (EINTR == errno))
392 {
393 LOG(VB_RECORD, LOG_ERR, LOC + "retry command read.");
394 continue; // errors that tell you to try again
395 }
396
397 LOG(VB_RECORD, LOG_ERR, LOC + "unknown error reading command.");
398 }
399 }
400 }
401
402 return true;
403}
404
405
406int main(int argc, char *argv[])
407{
409
410 if (!cmdline.Parse(argc, argv))
411 {
414 }
415
416 if (cmdline.toBool("showhelp"))
417 {
419 return GENERIC_EXIT_OK;
420 }
421
422 if (cmdline.toBool("showversion"))
423 {
425 return GENERIC_EXIT_OK;
426 }
427
428 bool loopinput = !cmdline.toBool("noloop");
429 int data_rate = cmdline.toInt("data_rate");
430
431 QCoreApplication a(argc, argv);
432 QCoreApplication::setApplicationName("mythfilerecorder");
433
434 int retval = cmdline.ConfigureLogging();
435 if (retval != GENERIC_EXIT_OK)
436 return retval;
437
438 QString filename = "";
439 if (!cmdline.toString("infile").isEmpty())
440 filename = cmdline.toString("infile");
441 else if (!cmdline.GetArgs().empty())
442 filename = cmdline.GetArgs().at(0);
443
445 recorder.Run(filename, data_rate, loopinput);
446
447 return GENERIC_EXIT_OK;
448}
449
450/* 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
MythCommFlagCommandLineParser cmdline
RemoteEncoder * recorder
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
def write(text, progress=True)
Definition: mythburn.py:307
STL namespace.