Skip to content

Commit

Permalink
ExternalRecorder: Add a JSON control option (APIv3)
Browse files Browse the repository at this point in the history
ExternalChannel: with APIv3, provide inputid, sourceid, chanid, freqid, atsc_major, atsc_minor, mplexid and recordid to external recorder.
  • Loading branch information
jpoet committed Sep 7, 2024
1 parent 4786cc3 commit 2856879
Show file tree
Hide file tree
Showing 7 changed files with 536 additions and 214 deletions.
80 changes: 74 additions & 6 deletions mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,84 @@ bool ExternalChannel::Tune(const QString &channum)

LOG(VB_CHANNEL, LOG_INFO, LOC + "Tuning to " + channum);

if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum,
result, m_tuneTimeout))
if (m_streamHandler->APIVersion() < 3)
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to Tune %1: %2").arg(channum, result));
return false;
if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum,
result, m_tuneTimeout))
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to Tune %1: %2").arg(channum, result));
return false;
}
m_backgroundTuning = result.startsWith("OK:InProgress");
}
else
{
QVariantMap cmd, vresult;
QByteArray response;

cmd["command"] = "TuneChannel";
cmd["channum"] = channum;
cmd["inputid"] = GetInputID();
cmd["sourceid"] = m_sourceId;

if (m_pParent)
{
ProgramInfo* prog = m_pParent->GetRecording();
if (prog)
{
uint recordid = prog->GetRecordingRuleID();
cmd["recordid"] = recordid;
}
}

uint chanid = 0;
QString tvformat;
QString modulation;
QString freqtable;
QString freqid;
int finetune = 0;
uint64_t frequency = 0;
QString dtv_si_std;
int mpeg_prog_num = 0;
uint atsc_major = 0;
uint atsc_minor = 0;
uint dvb_transportid = 0;
uint dvb_networkid = 0;
uint mplexid = 0;
bool commfree = false;

if (!ChannelUtil::GetChannelData(m_sourceId, chanid, channum,
tvformat, modulation, freqtable, freqid,
finetune, frequency, dtv_si_std,
mpeg_prog_num, atsc_major, atsc_minor,
dvb_transportid, dvb_networkid,
mplexid, commfree))
{
LOG(VB_GENERAL, LOG_ERR, LOC + " " +
QString("Failed to find channel in DB on input '%1' ")
.arg(m_inputId));
}
else
{
cmd["chanid"] = chanid;
cmd["freqid"] = freqid;
cmd["atsc_major"] = atsc_major;
cmd["atsc_minor"] = atsc_minor;
cmd["mplexid"] = mplexid;
}

if (!m_streamHandler->ProcessJson(cmd, vresult, response))
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to Tune %1: %2").arg(channum, QString(response)));
return false;
}
m_backgroundTuning = vresult["message"]
.toString().startsWith("InProgress");
}

UpdateDescription();
m_backgroundTuning = result.startsWith("OK:InProgress");

return true;
}
Expand Down
174 changes: 171 additions & 3 deletions mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
// Qt headers
#include <QString>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>

// MythTV headers
#include "config.h"
Expand Down Expand Up @@ -168,7 +170,7 @@ int ExternIO::Read(QByteArray & buffer, int maxlen, std::chrono::milliseconds ti
return len;
}

QString ExternIO::GetStatus(std::chrono::milliseconds timeout)
QByteArray ExternIO::GetStatus(std::chrono::milliseconds timeout)
{
if (Error())
{
Expand All @@ -194,7 +196,7 @@ QString ExternIO::GetStatus(std::chrono::milliseconds timeout)
LOG(VB_RECORD, LOG_DEBUG, QString("ExternIO::GetStatus '%1'")
.arg(msg));

return msg;
return msg.toUtf8();
}

int ExternIO::Write(const QByteArray & buffer)
Expand Down Expand Up @@ -876,7 +878,7 @@ bool ExternalStreamHandler::SetAPIVersion(void)
{
LOG(VB_RECORD, LOG_ERR, LOC +
QString("Bad response to 'APIVersion?' - '%1'. "
"Expecting 1 or 2").arg(result));
"Expecting 1, 2 or 3").arg(result));
m_apiVersion = 1;
}

Expand Down Expand Up @@ -1234,6 +1236,23 @@ bool ExternalStreamHandler::ProcessCommand(const QString & cmd,
{
QMutexLocker locker(&m_processLock);

if (m_apiVersion == 3)
{
QVariantMap vcmd, vresult;
QByteArray response;
QStringList tokens = cmd.split(':');
vcmd["command"] = tokens[0];
if (tokens.size() > 1)
vcmd["value"] = tokens[1];

LOG(VB_RECORD, LOG_DEBUG, LOC +
QString("Arguments: %1").arg(tokens.join("\n")));

bool r = ProcessJson(vcmd, vresult, response, timeout, retry_cnt);
result = QString("%1:%2").arg(vresult["status"].toString())
.arg(vresult["message"].toString());
return r;
}
if (m_apiVersion == 2)
return ProcessVer2(cmd, result, timeout, retry_cnt);
if (m_apiVersion == 1)
Expand Down Expand Up @@ -1492,6 +1511,155 @@ bool ExternalStreamHandler::ProcessVer2(const QString & command,
return false;
}

bool ExternalStreamHandler::ProcessJson(const QVariantMap & vmsg,
QVariantMap & elements,
QByteArray & response,
std::chrono::milliseconds timeout,
uint retry_cnt)
{
for (uint cnt = 0; cnt < retry_cnt; ++cnt)
{
QVariantMap query(vmsg);

uint serial = ++m_serialNo;
query["serial"] = serial;
QString cmd = query["command"].toString();

QJsonDocument qdoc;
qdoc = QJsonDocument::fromVariant(query);
QByteArray cmdbuf = qdoc.toJson(QJsonDocument::Compact);

LOG(VB_RECORD, LOG_DEBUG, LOC +
QString("ProcessJson: %1").arg(QString(cmdbuf)));

if (m_io->Error())
{
LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder in bad state: " +
m_io->ErrorString());
return false;
}

/* Send query */
m_io->Write(cmdbuf);
m_io->Write("\n");

MythTimer timer(MythTimer::kStartRunning);
while (timer.elapsed() < timeout)
{
response = m_io->GetStatus(timeout);
if (m_io->Error())
{
LOG(VB_GENERAL, LOG_ERR, LOC +
"Failed to read from External Recorder: " +
m_io->ErrorString());
m_bError = true;
return false;
}

if (!response.isEmpty())
{
QJsonParseError parseError;
QJsonDocument doc;

doc = QJsonDocument::fromJson(response, &parseError);

if (parseError.error != QJsonParseError::NoError)
{
LOG(VB_GENERAL, LOG_ERR, LOC +
QString("ExternalRecorder returned invalid JSON message: %1: %2\n%3\nfor\n%4")
.arg(parseError.offset).arg(parseError.errorString())
.arg(QString(response))
.arg(QString(cmdbuf)));
}
else
{
elements = doc.toVariant().toMap();
if (elements.find("serial") == elements.end())
continue;

serial = elements["serial"].toInt();
if (serial >= m_serialNo)
break;

if (elements.find("status") != elements.end() &&
elements["status"] != "OK")
{
LOG(VB_RECORD, LOG_WARNING, LOC + QString("%1: %2")
.arg(elements["status"].toString())
.arg(elements["message"].toString()));
}
}
}
}

if (timer.elapsed() >= timeout)
{
LOG(VB_RECORD, LOG_ERR, LOC +
QString("ProcessJson: Giving up waiting for response for "
"command '%2'").arg(QString(cmdbuf)));

}

if (serial > m_serialNo)
{
LOG(VB_RECORD, LOG_ERR, LOC +
QString("ProcessJson: Looking for serial no %1, "
"but received %2 for command '%2'")
.arg(QString::number(m_serialNo))
.arg(serial)
.arg(QString(cmdbuf)));
}
else if (elements.find("status") == elements.end())
{
LOG(VB_RECORD, LOG_ERR, LOC +
QString("ProcessJson: ExternalRecorder 'status' not found in %1")
.arg(QString(response)));
}
else
{
QString status = elements["status"].toString();
bool okay = (status == "OK");
if (okay || status == "WARN" || status == "ERR")
{
LogLevel_t level = LOG_INFO;

m_ioErrCnt = 0;
if (!okay)
level = LOG_WARNING;
else if (cmd == "SendBytes" ||
(cmd == "TuneStatus" &&
elements["message"] == "InProgress"))
level = LOG_DEBUG;

LOG(VB_RECORD, level,
LOC + QString("ProcessJson('%1') = %2:%3:%4 took %5ms %6")
.arg(QString(cmdbuf))
.arg(elements["serial"].toInt())
.arg(elements["status"].toString())
.arg(elements["message"].toString())
.arg(QString::number(timer.elapsed().count()))
.arg(okay ? "" : "<-- NOTE")
);

return okay;
}
LOG(VB_GENERAL, LOG_WARNING, LOC +
QString("External Recorder invalid response to '%1': '%2'")
.arg(QString(cmdbuf))
.arg(QString(response)));
}

if (++m_ioErrCnt > 10)
{
LOG(VB_GENERAL, LOG_ERR, LOC + "Too many I/O errors.");
m_bError = true;
break;
}
}

return false;
}

bool ExternalStreamHandler::CheckForError(void)
{
QString result;
Expand Down
11 changes: 9 additions & 2 deletions mythtv/libs/libmythtv/recorders/ExternalStreamHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <QFileInfo>
#include <QMutex>
#include <QMap>
#include <QVariantMap>
#include <QStringList>
#include <QTextStream>

Expand All @@ -35,7 +36,7 @@ class ExternIO

bool Ready(int fd, std::chrono::milliseconds timeout, const QString & what);
int Read(QByteArray & buffer, int maxlen, std::chrono::milliseconds timeout = 2500ms);
QString GetStatus(std::chrono::milliseconds timeout = 2500ms);
QByteArray GetStatus(std::chrono::milliseconds timeout = 2500ms);
int Write(const QByteArray & buffer);
bool Run(void);
bool Error(void) const { return !m_error.isEmpty(); }
Expand Down Expand Up @@ -67,7 +68,7 @@ class ExternIO

class ExternalStreamHandler : public StreamHandler
{
enum constants { MAX_API_VERSION = 2,
enum constants { MAX_API_VERSION = 3,
TS_PACKET_SIZE = 188,
PACKET_SIZE = TS_PACKET_SIZE * 8192,
TOO_FAST_SIZE = TS_PACKET_SIZE * 32768 };
Expand Down Expand Up @@ -112,6 +113,12 @@ class ExternalStreamHandler : public StreamHandler
std::chrono::milliseconds timeout, uint retry_cnt);
bool ProcessVer2(const QString & command, QString & result,
std::chrono::milliseconds timeout, uint retry_cnt);
bool ProcessJson(const QVariantMap & vmsg,
QVariantMap & result,
QByteArray & response,
std::chrono::milliseconds timeout = 4s,
uint retry_cnt = 3);
int APIVersion(void) const { return m_apiVersion; }

private:
int StreamingCount(void) const;
Expand Down
Loading

0 comments on commit 2856879

Please sign in to comment.