181 changes: 181 additions & 0 deletions mythtv/libs/libmythtv/opengl/mythopengltonemap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// MythTV
#include "mythlogging.h"
#include "mythopenglcomputeshaders.h"
#include "mythopengltonemap.h"

#define LOC QString("Tonemap: ")

#ifndef GL_SHADER_STORAGE_BUFFER
#define GL_SHADER_STORAGE_BUFFER 0x90D2
#endif
#ifndef GL_ALL_BARRIER_BITS
#define GL_ALL_BARRIER_BITS 0xFFFFFFFF
#endif
#ifndef GL_SHADER_IMAGE_ACCESS_BARRIER_BIT
#define GL_SHADER_IMAGE_ACCESS_BARRIER_BIT 0x00000020
#endif
#ifndef GL_STREAM_COPY
#define GL_STREAM_COPY 0x88E2
#endif
#ifndef GL_WRITE_ONLY
#define GL_WRITE_ONLY 0x88B9
#endif

MythOpenGLTonemap::MythOpenGLTonemap(MythRenderOpenGL *Render, VideoColourSpace *ColourSpace)
: QObject()
{
if (Render)
{
Render->IncrRef();
m_render = Render;
m_extra = Render->extraFunctions();
}

if (ColourSpace)
{
ColourSpace->IncrRef();
m_colourSpace = ColourSpace;
connect(m_colourSpace, &VideoColourSpace::Updated, this, &MythOpenGLTonemap::UpdateColourSpace);
}
}

MythOpenGLTonemap::~MythOpenGLTonemap()
{
if (m_render)
{
m_render->makeCurrent();
if (m_storageBuffer)
m_render->glDeleteBuffers(1, &m_storageBuffer);
delete m_shader;
delete m_texture;
m_render->doneCurrent();
m_render->DecrRef();
}

if (m_colourSpace)
m_colourSpace->DecrRef();
}

void MythOpenGLTonemap::UpdateColourSpace(bool PrimariesChanged)
{
(void)PrimariesChanged;
OpenGLLocker locker(m_render);
if (m_shader)
{
m_render->SetShaderProgramParams(m_shader, *m_colourSpace, "m_colourMatrix");
m_render->SetShaderProgramParams(m_shader, m_colourSpace->GetPrimaryMatrix(), "m_primaryMatrix");
}
}

MythVideoTexture* MythOpenGLTonemap::GetTexture(void)
{
return m_texture;
}

MythVideoTexture* MythOpenGLTonemap::Map(vector<MythVideoTexture *> &Inputs, QSize DisplaySize)
{
size_t size = Inputs.size();
if (!size || !m_render || !m_extra)
return nullptr;

OpenGLLocker locker(m_render);
bool changed = m_outputSize != DisplaySize;

if (!m_texture || changed)
if (!CreateTexture(DisplaySize))
return nullptr;

changed |= (m_inputCount != size) || (m_inputType != Inputs[0]->m_frameFormat) ||
(m_inputSize != Inputs[0]->m_size);

if (!m_shader || changed)
if (!CreateShader(size, Inputs[0]->m_frameFormat, Inputs[0]->m_size))
return nullptr;

if (!m_storageBuffer)
{
m_render->glGenBuffers(1, &m_storageBuffer);
if (!m_storageBuffer)
{
LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to allocate storage buffer");
return nullptr;
}
m_render->glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storageBuffer);
struct dummy { float a[2] {0.0F}; uint32_t b {0}; uint32_t c {0}; uint32_t d {0}; } buffer;
m_render->glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(dummy), &buffer, GL_STREAM_COPY);
m_render->glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}

m_render->EnableShaderProgram(m_shader);
for (size_t i = 0; i < size; ++i)
{
m_render->ActiveTexture(GL_TEXTURE0 + static_cast<GLuint>(i));
if (Inputs[i]->m_texture)
Inputs[i]->m_texture->bind();
else
m_render->glBindTexture(Inputs[i]->m_target, Inputs[i]->m_textureId);
}

m_extra->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_storageBuffer);
m_extra->glMemoryBarrier(GL_ALL_BARRIER_BITS);
m_extra->glBindImageTexture(0, m_texture->m_textureId, 0, GL_FALSE, 0, GL_WRITE_ONLY, QOpenGLTexture::RGBA16F);
m_extra->glDispatchCompute((static_cast<GLuint>(m_texture->m_size.width()) + 1) >> 3,
(static_cast<GLuint>(m_texture->m_size.height()) + 1) >> 3, 1);
m_extra->glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
m_extra->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
return m_texture;
}

bool MythOpenGLTonemap::CreateShader(size_t InputSize, VideoFrameType Type, QSize Size)
{
delete m_shader;
m_shader = nullptr;
m_inputSize = Size;

QString source = (m_render->isOpenGLES() ? "#version 310 es\n" : "#version 430\n");
if (format_is_420(Type) || format_is_422(Type) || format_is_444(Type))
source.append("#define YV12\n");
if (m_render->isOpenGLES() && ColorDepth(Type) > 8)
source.append("#define UNSIGNED\n");
source.append(GLSL430Tonemap);

m_shader = m_render->CreateComputeShader(source);
if (m_shader)
{
m_inputCount = InputSize;
m_inputType = Type;
m_render->EnableShaderProgram(m_shader);
for (size_t i = 0; i < InputSize; ++i)
m_shader->setUniformValue(QString("texture%1").arg(i).toLatin1().constData(), static_cast<GLuint>(i));
LOG(VB_GENERAL, LOG_INFO, QString("Created tonemapping compute shader (%1 inputs)")
.arg(InputSize));
UpdateColourSpace(false);
return true;
}

return false;
}

bool MythOpenGLTonemap::CreateTexture(QSize Size)
{
delete m_texture;
m_texture = nullptr;
m_outputSize = Size;
GLuint textureid = 0;
m_render->glGenTextures(1, &textureid);
if (!textureid)
return false;

m_texture = new MythVideoTexture(textureid);
m_texture->m_frameType = FMT_RGBA32;
m_texture->m_frameFormat = FMT_RGBA32;
m_texture->m_target = QOpenGLTexture::Target2D;
m_texture->m_size = Size;
m_texture->m_totalSize = m_render->GetTextureSize(Size, m_texture->m_target != QOpenGLTexture::TargetRectangle);
m_texture->m_vbo = m_render->CreateVBO(static_cast<int>(MythRenderOpenGL::kVertexSize));
m_extra->glBindTexture(m_texture->m_target, m_texture->m_textureId);
m_extra->glTexStorage2D(m_texture->m_target, 1, QOpenGLTexture::RGBA16F,
static_cast<GLsizei>(Size.width()), static_cast<GLsizei>(Size.height()));
m_render->SetTextureFilters(m_texture, QOpenGLTexture::Linear);
return m_texture != nullptr;
}
44 changes: 44 additions & 0 deletions mythtv/libs/libmythtv/opengl/mythopengltonemap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef MYTHOPENGLTONEMAP_H
#define MYTHOPENGLTONEMAP_H

// Qt
#include <QObject>

// MythTV
#include "opengl/mythrenderopengl.h"
#include "videocolourspace.h"
#include "mythvideotexture.h"

class MythOpenGLTonemap : public QObject
{
Q_OBJECT

public:
MythOpenGLTonemap(MythRenderOpenGL *Render, VideoColourSpace *ColourSpace);
~MythOpenGLTonemap();

MythVideoTexture* Map(vector<MythVideoTexture*> &Inputs, QSize DisplaySize);
MythVideoTexture* GetTexture(void);

public slots:
void UpdateColourSpace(bool PrimariesChanged);

private:
Q_DISABLE_COPY(MythOpenGLTonemap)

bool CreateShader(size_t InputSize, VideoFrameType Type, QSize Size);
bool CreateTexture(QSize Size);

MythRenderOpenGL* m_render { nullptr };
QOpenGLExtraFunctions* m_extra { nullptr };
VideoColourSpace* m_colourSpace { nullptr };
QOpenGLShaderProgram* m_shader { nullptr };
GLuint m_storageBuffer{ 0 };
MythVideoTexture* m_texture { nullptr };
size_t m_inputCount { 0 };
QSize m_inputSize { 0, 0 };
VideoFrameType m_inputType { FMT_NONE };
QSize m_outputSize { 0, 0 };
};

#endif // MYTHOPENGLTONEMAP_H
61 changes: 45 additions & 16 deletions mythtv/libs/libmythtv/opengl/mythopenglvideo.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// C/C++
#include <utility>

// MythTV
#include "mythcontext.h"
#include "tv.h"
#include "opengl/mythrenderopengl.h"
#include "mythavutil.h"
#include "mythopenglvideoshaders.h"
#include "mythopengltonemap.h"
#include "mythopenglvideo.h"

// std
#include <utility>

#define LOC QString("GLVid: ")
#define MAX_VIDEO_TEXTURES 10 // YV12 Kernel deinterlacer + 1

Expand Down Expand Up @@ -68,6 +69,7 @@ MythOpenGLVideo::~MythOpenGLVideo()

m_render->makeCurrent();
ResetFrameFormat();
delete m_toneMap;
m_render->doneCurrent();
m_render->DecrRef();
}
Expand Down Expand Up @@ -796,6 +798,10 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS
Frame->deinterlace_inuse2x = m_deinterlacer2x;
}

// Tonemapping can only render to a texture
if (m_toneMap)
resize |= ToneMap;

// Decide whether to use render to texture - for performance or quality
if (format_is_yuv(m_outputType) && !resize)
{
Expand Down Expand Up @@ -850,9 +856,10 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS
else if (!m_resizing && resize)
{
// framebuffer will be created as needed below
MythVideoTexture::SetTextureFilters(m_render, m_inputTextures, QOpenGLTexture::Nearest);
MythVideoTexture::SetTextureFilters(m_render, m_prevTextures, QOpenGLTexture::Nearest);
MythVideoTexture::SetTextureFilters(m_render, m_nextTextures, QOpenGLTexture::Nearest);
QOpenGLTexture::Filter filter = m_toneMap ? QOpenGLTexture::Linear : QOpenGLTexture::Nearest;
MythVideoTexture::SetTextureFilters(m_render, m_inputTextures, filter);
MythVideoTexture::SetTextureFilters(m_render, m_prevTextures, filter);
MythVideoTexture::SetTextureFilters(m_render, m_nextTextures, filter);
m_resizing = resize;
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Resizing from %1x%2 to %3x%4 for %5")
.arg(m_videoDispDim.width()).arg(m_videoDispDim.height())
Expand All @@ -863,15 +870,39 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS
// check hardware frames have the correct filtering
if (hwframes)
{
QOpenGLTexture::Filter filter = resize ? QOpenGLTexture::Nearest : QOpenGLTexture::Linear;
QOpenGLTexture::Filter filter = (resize && !m_toneMap) ? QOpenGLTexture::Nearest : QOpenGLTexture::Linear;
if (inputtextures[0]->m_filter != filter)
MythVideoTexture::SetTextureFilters(m_render, inputtextures, filter);
}

// texture coordinates
QRect trect(m_videoRect);

if (resize)
{
MythVideoTexture* nexttexture = nullptr;

// only render to the framebuffer if there is something to update
if (!useframebufferimage)
if (useframebufferimage)
{
if (m_toneMap)
{
nexttexture = m_toneMap->GetTexture();
trect = QRect(QPoint(0, 0), m_displayVideoRect.size());
}
else
{
nexttexture = m_frameBufferTexture;
}
}
else if (m_toneMap)
{
if (VERBOSE_LEVEL_CHECK(VB_GPU, LOG_INFO))
m_render->logDebugMarker(LOC + "RENDER_TO_TEXTURE");
nexttexture = m_toneMap->Map(inputtextures, m_displayVideoRect.size());
trect = QRect(QPoint(0, 0), m_displayVideoRect.size());
}
else
{
// render to texture stage
if (VERBOSE_LEVEL_CHECK(VB_GPU, LOG_INFO))
Expand All @@ -896,9 +927,9 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS

// coordinates
QRect vrect(QPoint(0, 0), m_videoDispDim);
QRect trect = vrect;
QRect trect2 = vrect;
if (FMT_YUY2 == m_outputType)
trect.setWidth(m_videoDispDim.width() >> 1);
trect2.setWidth(m_videoDispDim.width() >> 1);

// framebuffer
m_render->BindFramebuffer(m_frameBuffer);
Expand All @@ -911,12 +942,13 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS

// render
m_render->DrawBitmap(textures, numtextures, m_frameBuffer,
trect, vrect, m_shaders[program], 0);
trect2, vrect, m_shaders[program], 0);
nexttexture = m_frameBufferTexture;
}

// reset for next stage
inputtextures.clear();
inputtextures.push_back(m_frameBufferTexture);
inputtextures.push_back(nexttexture);
program = Default;
deinterlacing = false;
}
Expand All @@ -925,13 +957,10 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS
if (VERBOSE_LEVEL_CHECK(VB_GPU, LOG_INFO))
m_render->logDebugMarker(LOC + "RENDER_TO_SCREEN");

// texture coordinates
QRect trect(m_videoRect);

// discard stereoscopic fields
if (kStereoscopicModeSideBySideDiscard == Stereo)
trect = QRect(trect.left() >> 1, trect.top(), trect.width() >> 1, trect.height());
if (kStereoscopicModeTopAndBottomDiscard == Stereo)
else if (kStereoscopicModeTopAndBottomDiscard == Stereo)
trect = QRect(trect.left(), trect.top() >> 1, trect.width(), trect.height() >> 1);

// bind default framebuffer
Expand Down
6 changes: 5 additions & 1 deletion mythtv/libs/libmythtv/opengl/mythopenglvideo.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using std::vector;
using std::map;

class MythOpenGLTonemap;

class MythOpenGLVideo : public QObject
{
Q_OBJECT
Expand All @@ -38,7 +40,8 @@ class MythOpenGLVideo : public QObject
Deinterlacer = 0x001,
Sampling = 0x002,
Performance = 0x004,
Framebuffer = 0x008
Framebuffer = 0x008,
ToneMap = 0x010
};

Q_DECLARE_FLAGS(VideoResizing, VideoResize)
Expand Down Expand Up @@ -115,5 +118,6 @@ class MythOpenGLVideo : public QObject
long long m_discontinuityCounter { 0 }; ///< Check when to release reference frames after a skip
int m_lastRotation { 0 }; ///< Track rotation for pause frame
bool m_chromaUpsamplingFilter { false }; /// Attempt to fix Chroma Upsampling Error in shaders
MythOpenGLTonemap* m_toneMap { nullptr };
};
#endif // MYTH_OPENGL_VIDEO_H_
2 changes: 1 addition & 1 deletion mythtv/libs/libmythtv/opengl/mythvideotexture.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class QMatrix4x4;
class MythVideoTexture : public MythGLTexture
{
public:
explicit MythVideoTexture(GLuint Texture);
static vector<MythVideoTexture*> CreateTextures(MythRenderOpenGL* Context,
VideoFrameType Type,
VideoFrameType Format,
Expand Down Expand Up @@ -57,7 +58,6 @@ class MythVideoTexture : public MythGLTexture

protected:
explicit MythVideoTexture(QOpenGLTexture* Texture);
explicit MythVideoTexture(GLuint Texture);

private:
static vector<MythVideoTexture*> CreateHardwareTextures(MythRenderOpenGL* Context,
Expand Down
77 changes: 67 additions & 10 deletions mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,38 @@ bool ExternalChannel::Tune(const QString &channum)
return true;

QString result;

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

if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum, result,
20000))
if (m_tuneTimeout < 0)
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to Tune %1: %2").arg(channum).arg(result));
return false;
// When mythbackend first starts up, just retrive the
// tuneTimeout for subsequent tune requests.

if (!m_streamHandler->ProcessCommand("LockTimeout?", result))
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to retrieve LockTimeout: %1").arg(result));
m_tuneTimeout = 60000;
}
else
m_tuneTimeout = result.split(":")[1].toInt();

LOG(VB_CHANNEL, LOG_INFO, LOC + QString("Using Tune timeout of %1ms")
.arg(m_tuneTimeout));
}
else
{
LOG(VB_CHANNEL, LOG_INFO, LOC + "Tuning to " + channum);

if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum,
result, m_tuneTimeout))
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to Tune %1: %2").arg(channum).arg(result));
return false;
}

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

UpdateDescription();

return true;
}
Expand All @@ -124,3 +144,40 @@ bool ExternalChannel::EnterPowerSavingMode(void)
Close();
return true;
}

uint ExternalChannel::GetTuneStatus(void)
{

if (!m_backgroundTuning)
return 3;

LOG(VB_CHANNEL, LOG_DEBUG, LOC + QString("GetScriptStatus() %1")
.arg(m_systemStatus));

QString result;
int ret;

if (!m_streamHandler->ProcessCommand("TuneStatus?", result))
{
LOG(VB_CHANNEL, LOG_ERR, LOC + QString
("Failed to Tune: %1").arg(result));
ret = 2;
m_backgroundTuning = false;
}
else
{
if (result.startsWith("OK:Running"))
ret = 1;
else
{
ret = 3;
m_backgroundTuning = false;
}
UpdateDescription();
}

LOG(VB_CHANNEL, LOG_DEBUG, LOC + QString("GetScriptStatus() %1 -> %2")
.arg(m_systemStatus). arg(ret));

return ret;
}
4 changes: 4 additions & 0 deletions mythtv/libs/libmythtv/recorders/ExternalChannel.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,16 @@ class ExternalChannel : public DTVChannel

QString UpdateDescription(void);
QString GetDescription(void);
bool IsBackgroundTuning(void) const { return m_backgroundTuning; }
uint GetTuneStatus(void);

protected:
bool IsExternalChannelChangeSupported(void) override // ChannelBase
{ return true; }

private:
int m_tuneTimeout { -1 };
bool m_backgroundTuning {false};
QString m_device;
QStringList m_args;
ExternalStreamHandler *m_streamHandler {nullptr};
Expand Down
13 changes: 13 additions & 0 deletions mythtv/libs/libmythtv/recorders/ExternalSignalMonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ ExternalSignalMonitor::ExternalSignalMonitor(int db_cardnum,
LOG(VB_GENERAL, LOG_ERR, LOC + "Open failed");
else
m_lock_timeout = GetLockTimeout() * 1000;

if (GetExternalChannel()->IsBackgroundTuning())
m_scriptStatus.SetValue(1);
}

/** \fn ExternalSignalMonitor::~ExternalSignalMonitor()
Expand Down Expand Up @@ -105,6 +108,16 @@ void ExternalSignalMonitor::UpdateValues(void)
return;
}

if (GetExternalChannel()->IsBackgroundTuning())
{
QMutexLocker locker(&m_statusLock);
if (m_scriptStatus.GetValue() < 2)
m_scriptStatus.SetValue(GetExternalChannel()->GetTuneStatus());

if (!m_scriptStatus.IsGood())
return;
}

if (m_stream_handler_started)
{
if (!m_stream_handler->IsRunning())
Expand Down
11 changes: 8 additions & 3 deletions mythtv/programs/mythbackend/scheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3660,7 +3660,7 @@ void Scheduler::UpdateManuals(uint recordid)

query.prepare(QString("SELECT type,title,subtitle,description,"
"station,startdate,starttime,"
"enddate,endtime,season,episode,inetref "
"enddate,endtime,season,episode,inetref,last_record "
"FROM %1 WHERE recordid = :RECORDID").arg(m_recordTable));
query.bindValue(":RECORDID", recordid);
if (!query.exec() || query.size() != 1)
Expand All @@ -3687,6 +3687,10 @@ void Scheduler::UpdateManuals(uint recordid)
int episode = query.value(10).toInt();
QString inetref = query.value(11).toString();

// A bit of a hack: mythconverg.record.last_record can be used by
// the services API to propegate originalairdate information.
QDate originalairdate = QDate(query.value(12).toDate());

if (description.isEmpty())
description = startdt.toLocalTime().toString();

Expand Down Expand Up @@ -3753,10 +3757,10 @@ void Scheduler::UpdateManuals(uint recordid)

query.prepare("REPLACE INTO program (chanid, starttime, endtime,"
" title, subtitle, description, manualid,"
" season, episode, inetref, generic) "
" season, episode, inetref, originalairdate, generic) "
"VALUES (:CHANID, :STARTTIME, :ENDTIME, :TITLE,"
" :SUBTITLE, :DESCRIPTION, :RECORDID, "
" :SEASON, :EPISODE, :INETREF, 1)");
" :SEASON, :EPISODE, :INETREF, :ORIGINALAIRDATE, 1)");
query.bindValue(":CHANID", id);
query.bindValue(":STARTTIME", startdt);
query.bindValue(":ENDTIME", startdt.addSecs(duration));
Expand All @@ -3766,6 +3770,7 @@ void Scheduler::UpdateManuals(uint recordid)
query.bindValue(":SEASON", season);
query.bindValue(":EPISODE", episode);
query.bindValue(":INETREF", inetref);
query.bindValue(":ORIGINALAIRDATE", originalairdate);
query.bindValue(":RECORDID", recordid);
if (!query.exec())
{
Expand Down
4 changes: 4 additions & 0 deletions mythtv/programs/mythbackend/services/dvr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,7 @@ uint Dvr::AddRecordSchedule (
uint nPreferredInput,
int nStartOffset,
int nEndOffset,
QDateTime lastrectsRaw,
QString sDupMethod,
QString sDupIn,
uint nFilter,
Expand All @@ -1113,6 +1114,7 @@ uint Dvr::AddRecordSchedule (
{
QDateTime recstartts = recstarttsRaw.toUTC();
QDateTime recendts = recendtsRaw.toUTC();
QDateTime lastrects = lastrectsRaw.toUTC();
RecordingRule rule;
rule.LoadTemplate("Default");

Expand Down Expand Up @@ -1199,6 +1201,8 @@ uint Dvr::AddRecordSchedule (

rule.m_transcoder = nTranscoder;

rule.m_lastRecorded = lastrects;

QString msg;
if (!rule.IsValid(msg))
throw msg;
Expand Down
4 changes: 3 additions & 1 deletion mythtv/programs/mythbackend/services/dvr.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class Dvr : public DvrServices
uint PreferredInput,
int StartOffset,
int EndOffset,
QDateTime lastrectsRaw,
QString DupMethod,
QString DupIn,
uint Filter,
Expand Down Expand Up @@ -491,7 +492,8 @@ class ScriptableDvr : public QObject
rule->Inetref(), rule->Type(),
rule->SearchType(), rule->RecPriority(),
rule->PreferredInput(), rule->StartOffset(),
rule->EndOffset(), rule->DupMethod(),
rule->EndOffset(), rule->LastRecorded(),
rule->DupMethod(),
rule->DupIn(), rule->Filter(),
rule->RecProfile(), rule->RecGroup(),
rule->StorageGroup(), rule->PlayGroup(),
Expand Down
19 changes: 17 additions & 2 deletions mythtv/programs/mythexternrecorder/MythExternControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ void Commands::TuneChannel(const QString & serial, const QString & channum)
emit m_parent->TuneChannel(serial, channum);
}

void Commands::TuneStatus(const QString & serial)
{
emit m_parent->TuneStatus(serial);
}

void Commands::LoadChannels(const QString & serial)
{
emit m_parent->LoadChannels(serial);
Expand All @@ -188,6 +193,11 @@ void Commands::NextChannel(const QString & serial)
emit m_parent->NextChannel(serial);
}

void Commands::Cleanup(void)
{
emit m_parent->Cleanup();
}

bool Commands::SendStatus(const QString & command, const QString & status)
{
int len = write(2, status.toUtf8().constData(), status.size());
Expand Down Expand Up @@ -309,7 +319,7 @@ bool Commands::ProcessCommand(const QString & cmd)
else
SendStatus(cmd, tokens[0], "OK:20");
}
else if (tokens[1].startsWith("LockTimeout"))
else if (tokens[1].startsWith("LockTimeout?"))
{
LockTimeout(tokens[0]);
}
Expand Down Expand Up @@ -352,11 +362,15 @@ bool Commands::ProcessCommand(const QString & cmd)
}
else if (tokens[1].startsWith("TuneChannel"))
{
if (tokens.size() > 1)
if (tokens.size() > 2)
TuneChannel(tokens[0], tokens[2]);
else
SendStatus(cmd, tokens[0], "ERR:Missing channum");
}
else if (tokens[1].startsWith("TuneStatus?"))
{
TuneStatus(tokens[0]);
}
else if (tokens[1].startsWith("LoadChannels"))
{
LoadChannels(tokens[0]);
Expand Down Expand Up @@ -385,6 +399,7 @@ bool Commands::ProcessCommand(const QString & cmd)
StopStreaming(tokens[0], true);
m_parent->Terminate();
SendStatus(cmd, tokens[0], "OK:Terminating");
Cleanup();
}
else if (tokens[1].startsWith("FlowControl?"))
{
Expand Down
4 changes: 4 additions & 0 deletions mythtv/programs/mythexternrecorder/MythExternControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ class Commands : public QObject
void HasPictureAttributes(const QString & serial) const;
void SetBlockSize(const QString & serial, int blksz);
void TuneChannel(const QString & serial, const QString & channum);
void TuneStatus(const QString & serial);
void LoadChannels(const QString & serial);
void FirstChannel(const QString & serial);
void NextChannel(const QString & serial);
void Cleanup(void);

private:
std::thread m_thread;
Expand Down Expand Up @@ -145,9 +147,11 @@ class MythExternControl : public QObject
void HasPictureAttributes(const QString & serial) const;
void SetBlockSize(const QString & serial, int blksz);
void TuneChannel(const QString & serial, const QString & channum);
void TuneStatus(const QString & serial);
void LoadChannels(const QString & serial);
void FirstChannel(const QString & serial);
void NextChannel(const QString & serial);
void Cleanup(void);

public slots:
void SetDescription(const QString & desc) { m_desc = desc; }
Expand Down
213 changes: 153 additions & 60 deletions mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <QFileInfo>
#include <QProcess>
#include <QtCore/QtCore>
#include <unistd.h>

#define LOC Desc()

Expand All @@ -42,8 +43,7 @@ MythExternRecApp::MythExternRecApp(QString command,
if (m_configIni.isEmpty() || !config())
m_recDesc = m_recCommand;

if (m_tuneCommand.isEmpty())
m_command = m_recCommand;
m_command = m_recCommand;

LOG(VB_CHANNEL, LOG_INFO, LOC +
QString("Channels in '%1', Tuner: '%2', Scanner: '%3'")
Expand Down Expand Up @@ -85,6 +85,7 @@ bool MythExternRecApp::config(void)

m_recCommand = settings.value("RECORDER/command").toString();
m_recDesc = settings.value("RECORDER/desc").toString();
m_cleanup = settings.value("RECORDER/cleanup").toString();
m_tuneCommand = settings.value("TUNER/command", "").toString();
m_channelsIni = settings.value("TUNER/channels", "").toString();
m_lockTimeout = settings.value("TUNER/timeout", "").toInt();
Expand Down Expand Up @@ -177,29 +178,31 @@ bool MythExternRecApp::Open(void)
return true;
}

void MythExternRecApp::TerminateProcess(void)
void MythExternRecApp::TerminateProcess(QProcess & proc, const QString & desc)
{
if (m_proc.state() == QProcess::Running)
if (proc.state() == QProcess::Running)
{
LOG(VB_RECORD, LOG_INFO, LOC +
QString("Sending SIGINT to %1").arg(m_proc.pid()));
kill(m_proc.pid(), SIGINT);
m_proc.waitForFinished(5000);
QString("Sending SIGINT to %1(%2)").arg(desc).arg(proc.pid()));
kill(proc.pid(), SIGINT);
proc.waitForFinished(5000);
}
if (m_proc.state() == QProcess::Running)
if (proc.state() == QProcess::Running)
{
LOG(VB_RECORD, LOG_INFO, LOC +
QString("Sending SIGTERM to %1").arg(m_proc.pid()));
m_proc.terminate();
m_proc.waitForFinished();
QString("Sending SIGTERM to %1(%2)").arg(desc).arg(proc.pid()));
proc.terminate();
proc.waitForFinished();
}
if (m_proc.state() == QProcess::Running)
if (proc.state() == QProcess::Running)
{
LOG(VB_RECORD, LOG_INFO, LOC +
QString("Sending SIGKILL to %1").arg(m_proc.pid()));
m_proc.kill();
m_proc.waitForFinished();
QString("Sending SIGKILL to %1(%2)").arg(desc).arg(proc.pid()));
proc.kill();
proc.waitForFinished();
}

return;
}

Q_SLOT void MythExternRecApp::Close(void)
Expand All @@ -212,10 +215,16 @@ Q_SLOT void MythExternRecApp::Close(void)
std::this_thread::sleep_for(std::chrono::microseconds(50));
}

if (m_tuneProc.state() == QProcess::Running)
{
m_tuneProc.closeReadChannel(QProcess::StandardOutput);
TerminateProcess(m_tuneProc, "App");
}

if (m_proc.state() == QProcess::Running)
{
m_proc.closeReadChannel(QProcess::StandardOutput);
TerminateProcess();
TerminateProcess(m_proc, "App");
std::this_thread::sleep_for(std::chrono::microseconds(50));
}

Expand Down Expand Up @@ -249,12 +258,45 @@ void MythExternRecApp::Run(void)
if (m_proc.state() == QProcess::Running)
{
m_proc.closeReadChannel(QProcess::StandardOutput);
TerminateProcess();
TerminateProcess(m_proc, "App");
}

emit Done();
}

Q_SLOT void MythExternRecApp::Cleanup(void)
{
m_tunedChannel.clear();

if (m_cleanup.isEmpty())
return;

QString cmd = m_cleanup;

LOG(VB_RECORD, LOG_WARNING, LOC +
QString(" Beginning cleanup: '%1'").arg(cmd));

QProcess cleanup;
cleanup.start(cmd);
if (!cleanup.waitForStarted())
{
LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start cleanup process: "
+ ENO);
return;
}
cleanup.waitForFinished(5000);
if (cleanup.state() == QProcess::NotRunning)
{
if (cleanup.exitStatus() != QProcess::NormalExit)
{
LOG(VB_RECORD, LOG_ERR, LOC + ": Cleanup process failed: " + ENO);
return;
}
}

LOG(VB_RECORD, LOG_INFO, LOC + ": Cleanup finished.");
}

Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial)
{
if (m_channelsIni.isEmpty())
Expand Down Expand Up @@ -379,55 +421,72 @@ Q_SLOT void MythExternRecApp::NextChannel(const QString & serial)
Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
const QString & channum)
{
if (m_channelsIni.isEmpty())
if (m_tuneCommand.isEmpty())
{
LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
emit SendMessage("TuneChannel", serial, "ERR:No channels configured.");
LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured.");
emit SendMessage("TuneChannel", serial, "ERR:No 'tuner' configured.");
return;
}

QSettings settings(m_channelsIni, QSettings::IniFormat);
settings.beginGroup(channum);
if (m_tunedChannel == channum)
{
LOG(VB_CHANNEL, LOG_INFO, LOC +
QString("TuneChanne: Already on %1").arg(channum));
emit SendMessage("TuneChannel", serial,
QString("OK:Tunned to %1").arg(channum));
return;
}

m_desc = m_recDesc;
m_command = m_recCommand;

QString url(settings.value("URL").toString());
QString tune = m_tuneCommand;
QString url;

if (url.isEmpty())
if (!m_channelsIni.isEmpty())
{
QString msg = QString("Channel number [%1] is missing a URL.")
.arg(channum);
QSettings settings(m_channelsIni, QSettings::IniFormat);
settings.beginGroup(channum);

LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg);
url = settings.value("URL").toString();

emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(msg));
return;
}
if (url.isEmpty())
{
QString msg = QString("Channel number [%1] is missing a URL.")
.arg(channum);

if (!m_tuneCommand.isEmpty())
{
// Repalce URL in command and execute it
QString tune = m_tuneCommand;
tune.replace("%URL%", url);
LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg);
}
else
tune.replace("%URL%", url);

if (system(tune.toUtf8().constData()) != 0)
if (!url.isEmpty() && m_command.indexOf("%URL%") >= 0)
{
QString errmsg = QString("'%1' failed: ").arg(tune) + ENO;
LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(errmsg));
return;
m_command.replace("%URL%", url);
LOG(VB_CHANNEL, LOG_DEBUG, LOC +
QString(": '%URL%' replaced with '%1' in cmd: '%2'")
.arg(url).arg(m_command));
}
LOG(VB_CHANNEL, LOG_INFO, LOC +
QString(": TuneChannel, ran '%1'").arg(tune));

m_desc.replace("%CHANNAME%", settings.value("NAME").toString());
m_desc.replace("%CALLSIGN%", settings.value("CALLSIGN").toString());

settings.endGroup();
}

// Replace URL in recorder command
m_command = m_recCommand;
if (m_tuneProc.state() == QProcess::Running)
TerminateProcess(m_tuneProc, "Tune");

if (!url.isEmpty() && m_command.indexOf("%URL%") >= 0)
tune.replace("%CHANNUM%", channum);
m_command.replace("%CHANNUM%", channum);

m_tuneProc.start(tune);
if (!m_tuneProc.waitForStarted())
{
m_command.replace("%URL%", url);
LOG(VB_CHANNEL, LOG_DEBUG, LOC +
QString(": '%URL%' replaced with '%1' in cmd: '%2'")
.arg(url).arg(m_command));
QString errmsg = QString("Tune `%1` failed: ").arg(tune) + ENO;
LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(errmsg));
return;
}

if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >= 0)
Expand All @@ -446,39 +505,73 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
.arg(m_logging).arg(m_command));
}

m_desc = m_recDesc;
m_desc.replace("%URL%", url);
m_desc.replace("%CHANNUM%", channum);
m_desc.replace("%CHANNAME%", settings.value("NAME").toString());
m_desc.replace("%CALLSIGN%", settings.value("CALLSIGN").toString());
m_tuningChannel = channum;

settings.endGroup();
LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Started `%1` URL '%2'")
.arg(tune).arg(url));
emit SendMessage("TuneChannel", serial,
QString("OK:Started `%1`").arg(tune));
}

LOG(VB_CHANNEL, LOG_INFO, LOC +
QString(": TuneChannel %1: URL '%2'").arg(channum).arg(url));
m_tuned = true;
Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial)
{
if (m_tuneProc.state() == QProcess::Running)
{
LOG(VB_CHANNEL, LOG_INFO, LOC +
QString(": Tune process(%1) still running").arg(m_tuneProc.pid()));
emit SendMessage("TuneStatus", serial, "OK:Running");
return;
}

if (m_tuneProc.exitStatus() != QProcess::NormalExit)
{
QString errmsg = QString("'%1' failed: ")
.arg(m_tuneProc.program()) + ENO;
LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
emit SendMessage("TuneStatus", serial,
QString("ERR:%1").arg(errmsg));
return;
}

m_tunedChannel = m_tuningChannel;
m_tuningChannel.clear();

LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Tuned %1").arg(m_tunedChannel));
emit SetDescription(Desc());
emit SendMessage("TuneChannel", serial,
QString("OK:Tunned to %1").arg(channum));
QString("OK:Tuned to %1").arg(m_tunedChannel));
}

Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial)
{
if (!Open())
{
LOG(VB_CHANNEL, LOG_WARNING, LOC +
"Cannot read LockTimeout from config file.");
emit SendMessage("LockTimeout", serial, "ERR: Not open");
return;
}

if (m_lockTimeout > 0)
{
LOG(VB_CHANNEL, LOG_INFO, LOC +
QString("Using configured LockTimeout of %1").arg(m_lockTimeout));
emit SendMessage("LockTimeout", serial,
QString("OK:%1").arg(m_lockTimeout));
return;
}
LOG(VB_CHANNEL, LOG_INFO, LOC +
"No LockTimeout defined in config, defaulting to 12000ms");
emit SendMessage("LockTimeout", serial, QString("OK:%1")
.arg(m_scanCommand.isEmpty() ? 12000 : 120000));
}

Q_SLOT void MythExternRecApp::HasTuner(const QString & serial)
{
emit SendMessage("HasTuner", serial, QString("OK:%1")
.arg(m_channelsIni.isEmpty() ? "No" : "Yes"));
.arg(m_tuneCommand.isEmpty() ? "No" : "Yes"));
}

Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial)
Expand All @@ -495,7 +588,7 @@ Q_SLOT void MythExternRecApp::SetBlockSize(const QString & serial, int blksz)
Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial)
{
m_streaming = true;
if (!m_tuned && !m_channelsIni.isEmpty())
if (m_tunedChannel.isEmpty() && !m_channelsIni.isEmpty())
{
LOG(VB_RECORD, LOG_ERR, LOC + ": No channel has been tuned");
emit SendMessage("StartStreaming", serial,
Expand Down Expand Up @@ -549,7 +642,7 @@ Q_SLOT void MythExternRecApp::StopStreaming(const QString & serial, bool silent)
m_streaming = false;
if (m_proc.state() == QProcess::Running)
{
TerminateProcess();
TerminateProcess(m_proc, "App");

LOG(VB_RECORD, LOG_INFO, LOC + ": External application terminated.");
if (silent)
Expand Down
9 changes: 7 additions & 2 deletions mythtv/programs/mythexternrecorder/MythExternRecApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,19 @@ class MythExternRecApp : public QObject
void StopStreaming(const QString & serial, bool silent);
void LockTimeout(const QString & serial);
void HasTuner(const QString & serial);
void Cleanup(void);
void LoadChannels(const QString & serial);
void FirstChannel(const QString & serial);
void NextChannel(const QString & serial);

void TuneChannel(const QString & serial, const QString & channum);
void TuneStatus(const QString & serial);
void HasPictureAttributes(const QString & serial);
void SetBlockSize(const QString & serial, int blksz);

protected:
void GetChannel(const QString & serial, const QString & func);
void TerminateProcess(void);
void TerminateProcess(QProcess & proc, const QString & desc);

private:
bool config(void);
Expand All @@ -97,12 +99,14 @@ class MythExternRecApp : public QObject

QProcess m_proc;
QString m_command;
QString m_cleanup;

QString m_recCommand;
QString m_recDesc;

QMap<QString, QString> m_appEnv;

QProcess m_tuneProc;
QString m_tuneCommand;
QString m_channelsIni;
uint m_lockTimeout { 0 };
Expand All @@ -115,7 +119,8 @@ class MythExternRecApp : public QObject
QString m_configIni;
QString m_desc;

bool m_tuned { false };
QString m_tuningChannel;
QString m_tunedChannel;

// Channel scanning
QSettings *m_chanSettings { nullptr };
Expand Down
2 changes: 1 addition & 1 deletion mythtv/programs/mythexternrecorder/commandlineparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ MythExternRecorderCommandLineParser::MythExternRecorderCommandLineParser() :

QString MythExternRecorderCommandLineParser::GetHelpHeader(void) const
{
return "MythFileRecorder is a go-between app which interfaces "
return "mythexternrecorder is a go-between app which interfaces "
"between a recording device and mythbackend.";
}

Expand Down
4 changes: 4 additions & 0 deletions mythtv/programs/mythexternrecorder/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ int main(int argc, char *argv[])
process, &MythExternRecApp::LockTimeout);
QObject::connect(control, &MythExternControl::HasTuner,
process, &MythExternRecApp::HasTuner);
QObject::connect(control, &MythExternControl::Cleanup,
process, &MythExternRecApp::Cleanup);
QObject::connect(control, &MythExternControl::LoadChannels,
process, &MythExternRecApp::LoadChannels);
QObject::connect(control, &MythExternControl::FirstChannel,
Expand All @@ -120,6 +122,8 @@ int main(int argc, char *argv[])
process, &MythExternRecApp::NextChannel);
QObject::connect(control, &MythExternControl::TuneChannel,
process, &MythExternRecApp::TuneChannel);
QObject::connect(control, &MythExternControl::TuneStatus,
process, &MythExternRecApp::TuneStatus);
QObject::connect(control, &MythExternControl::HasPictureAttributes,
process, &MythExternRecApp::HasPictureAttributes);
QObject::connect(control, &MythExternControl::SetBlockSize,
Expand Down