77 changes: 24 additions & 53 deletions mythtv/libs/libmythbase/loggingserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@ static QMutex logMsgListMutex;
static LogMessageList logMsgList;
static QWaitCondition logMsgListNotEmpty;

#define TIMESTAMP_MAX 30
#define MAX_STRING_LENGTH (LOGLINE_MAX+120)

/// \brief LoggerBase class constructor. Adds the new logger instance to the
/// loggerMap.
/// \param string a C-string of the handle for this instance (NULL if unused)
Expand Down Expand Up @@ -179,48 +176,36 @@ void FileLogger::reopen(void)
/// \param item LoggingItem containing the log message to process
bool FileLogger::logmsg(LoggingItem *item)
{
char line[MAX_STRING_LENGTH];
char usPart[9];
char timestamp[TIMESTAMP_MAX];

if (!m_opened)
return false;

time_t epoch = item->epoch();
struct tm tm {};
localtime_r(&epoch, &tm);

strftime(timestamp, TIMESTAMP_MAX-8, "%Y-%m-%d %H:%M:%S",
(const struct tm *)&tm);
snprintf( usPart, 9, ".%06d", (int)(item->usec()) );
strcat( timestamp, usPart );

char shortname = '-';
{
QMutexLocker locker(&loglevelMapMutex);
LoglevelMap::iterator it = loglevelMap.find(item->level());
if (it != loglevelMap.end())
shortname = (*it)->shortname;
}
QString timestamp = item->getTimestampUs();
QChar shortname = item->getLevelChar();

std::string line;
if( item->tid() )
{
snprintf( line, MAX_STRING_LENGTH,
"%s %c [%d/%" PREFIX64 "d] %s %s:%d (%s) - %s\n",
timestamp, shortname, item->pid(), item->tid(),
item->rawThreadName(), item->rawFile(), item->line(),
item->rawFunction(), item->rawMessage() );
line = qPrintable(QString("%1 %2 [%3/%4] %5 %6:%7 (%8) - %9\n")
.arg(timestamp, shortname,
QString::number(item->pid()),
QString::number(item->tid()),
item->threadName(), item->file(),
QString::number(item->line()),
item->function(),
item->message()));
}
else
{
snprintf( line, MAX_STRING_LENGTH,
"%s %c [%d] %s %s:%d (%s) - %s\n",
timestamp, shortname, item->pid(), item->rawThreadName(),
item->rawFile(), item->line(), item->rawFunction(),
item->rawMessage() );
line = qPrintable(QString("%1 %2 [%3] %5 %6:%7 (%8) - %9\n")
.arg(timestamp, shortname,
QString::number(item->pid()),
item->threadName(), item->file(),
QString::number(item->line()),
item->function(),
item->message()));
}

int result = write(m_fd, line, strlen(line));
int result = write(m_fd, line.data(), line.size());

if( result == -1 )
{
Expand Down Expand Up @@ -283,18 +268,11 @@ bool SyslogLogger::logmsg(LoggingItem *item)
if (!m_opened || item->facility() <= 0)
return false;

char shortname = '-';

{
QMutexLocker locker(&loglevelMapMutex);
LoglevelDef *lev = loglevelMap.value(item->level(), nullptr);
if (lev != nullptr)
shortname = lev->shortname;
}
char shortname = item->getLevelChar();
syslog(item->level() | item->facility(), "%s[%d]: %c %s %s:%d (%s) %s",
item->rawAppName(), item->pid(), shortname, item->rawThreadName(),
item->rawFile(), item->line(), item->rawFunction(),
item->rawMessage());
qPrintable(item->message()));

return true;
}
Expand Down Expand Up @@ -338,7 +316,7 @@ JournalLogger *JournalLogger::create(QMutex *mutex)
bool JournalLogger::logmsg(LoggingItem *item)
{
sd_journal_send(
"MESSAGE=%s", item->rawMessage(),
"MESSAGE=%s", qUtf8Printable(item->message()),
"PRIORITY=%d", item->level(),
"CODE_FILE=%s", item->rawFile(),
"CODE_LINE=%d", item->line(),
Expand Down Expand Up @@ -458,14 +436,7 @@ bool DatabaseLogger::logmsg(LoggingItem *item)
/// \param item LoggingItem containing the log message to insert
bool DatabaseLogger::logqmsg(MSqlQuery &query, LoggingItem *item)
{
char timestamp[TIMESTAMP_MAX];

time_t epoch = item->epoch();
struct tm tm {};
localtime_r(&epoch, &tm);

strftime(timestamp, TIMESTAMP_MAX-8, "%Y-%m-%d %H:%M:%S",
(const struct tm *)&tm);
QString timestamp = item->getTimestamp();

query.bindValue(":TID", item->tid());
query.bindValue(":THREAD", item->threadName());
Expand Down Expand Up @@ -615,7 +586,7 @@ void DBLoggerThread::run(void)
if (!item)
continue;

if (item->message()[0] != QChar('\0'))
if (!item->message().isEmpty())
{
qLock.unlock();
bool logged = m_logger->logqmsg(*query, item);
Expand Down
24 changes: 17 additions & 7 deletions mythtv/libs/libmythbase/mythlogging.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,42 @@ extern "C" {
// Neither of them will lock the calling thread other than momentarily to put
// the log message onto a queue.
#ifdef __cplusplus
#define LOG(_MASK_, _LEVEL_, _STRING_) \
#define LOG(_MASK_, _LEVEL_, _QSTRING_) \
do { \
if (VERBOSE_LEVEL_CHECK((_MASK_), (_LEVEL_)) && ((_LEVEL_)>=0)) \
{ \
LogPrintLine(_MASK_, _LEVEL_, \
__FILE__, __LINE__, __FUNCTION__, 1, \
qPrintable(_STRING_)); \
__FILE__, __LINE__, __FUNCTION__, \
_QSTRING_); \
} \
} while (false)
#else
#define LOG(_MASK_, _LEVEL_, _FORMAT_, ...) \
do { \
if (VERBOSE_LEVEL_CHECK((_MASK_), (_LEVEL_)) && ((_LEVEL_)>=0)) \
{ \
LogPrintLine(_MASK_, _LEVEL_, \
__FILE__, __LINE__, __FUNCTION__, 0, \
LogPrintLineC(_MASK_, _LEVEL_, \
__FILE__, __LINE__, __FUNCTION__, \
(const char *)_FORMAT_, ##__VA_ARGS__); \
} \
} while (0)
#endif

/* Define the external prototype */
#ifdef __cplusplus
MBASE_PUBLIC void LogPrintLine( uint64_t mask, LogLevel_t level,
const char *file, int line,
const char *function, int fromQString,
const char *format, ... );
const char *function,
QString message);
extern "C" {
#endif
MBASE_PUBLIC void LogPrintLineC( uint64_t mask, LogLevel_t level,
const char *file, int line,
const char *function,
const char *format, ...);
#ifdef __cplusplus
}
#endif

extern MBASE_PUBLIC LogLevel_t logLevel;
extern MBASE_PUBLIC uint64_t verboseMask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ QMAKE_LFLAGS += -Wl,$$_RPATH_$(PWD)/../..

!using_libexiv_external {
LIBS += -L../../../../external/libexiv2 -lmythexiv2-0.28
QMAKE_LFLAGS += -Wl,$$_RPATH_$(PWD)/../../../../external/libexiv2
freebsd: LIBS += -lexpat -lprocstat
darwin: LIBS += -lexpat -liconv -lz
QMAKE_LFLAGS += -Wl,$$_RPATH_$(PWD)/../../../../external/libexiv2 -lexpat
freebsd: LIBS += -lprocstat
darwin: LIBS += -liconv -lz
}

# Input
Expand Down
1 change: 1 addition & 0 deletions mythtv/libs/libmythservicecontracts/services/dvrServices.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ class SERVICE_PUBLIC DvrServices : public Service //, public QScriptable ???
uint PreferredInput,
int StartOffset,
int EndOffset,
QDateTime LastRecorded,
QString DupMethod,
QString DupIn,
uint Filter,
Expand Down
92 changes: 56 additions & 36 deletions mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ MythCodecID MythNVDECContext::GetSupportedCodec(AVCodecContext **Context,

cudaVideoChromaFormat cudaformat = cudaVideoChromaFormat_Monochrome;
VideoFrameType type = PixelFormatToFrameType((*Context)->pix_fmt);
uint depth = static_cast<uint>(ColorDepth(type) - 8);
QString desc = QString("'%1 %2 %3 Depth:%4 %5x%6'")
.arg(codecstr).arg(profile).arg(pixfmt).arg(depth + 8)
.arg((*Context)->width).arg((*Context)->height);

// N.B. on stream changes format is set to CUDA/NVDEC. This may break if the new
// stream has an unsupported chroma but the decoder should fail gracefully - just later.
if ((FMT_NVDEC == type) || (format_is_420(type)))
Expand All @@ -74,58 +79,60 @@ MythCodecID MythNVDECContext::GetSupportedCodec(AVCodecContext **Context,
else if (format_is_444(type))
cudaformat = cudaVideoChromaFormat_444;

uint depth = static_cast<uint>(ColorDepth(type) - 8);
bool supported = false;

if ((cudacodec == cudaVideoCodec_NumCodecs) || (cudaformat == cudaVideoChromaFormat_Monochrome))
{
LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "Unknown codec or format");
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC does NOT support %1").arg(desc));
return failure;
}

// iterate over known decoder capabilities
bool supported = false;
const std::vector<MythNVDECCaps>& profiles = MythNVDECContext::GetProfiles();
for (auto cap : profiles)
{
if (cap.Supports(cudacodec, cudaformat, depth, (*Context)->width, (*Context)->width))
if (cap.Supports(cudacodec, cudaformat, depth, (*Context)->width, (*Context)->height))
{
supported = true;
break;
}
}

QString desc = QString("'%1 %2 %3 Depth:%4 %5x%6'")
.arg(codecstr).arg(profile).arg(pixfmt).arg(depth + 8)
.arg((*Context)->width).arg((*Context)->height);
if (!supported)
{
LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "No matching profile support");
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC does NOT support %1").arg(desc));
return failure;
}

AvFormatDecoder *decoder = dynamic_cast<AvFormatDecoder*>(reinterpret_cast<DecoderBase*>((*Context)->opaque));
// and finally try and retrieve the actual FFmpeg decoder
if (supported && decoder)
QString name = QString((*Codec)->name) + "_cuvid";
if (name == "mpeg2video_cuvid")
name = "mpeg2_cuvid";
for (int i = 0; ; i++)
{
for (int i = 0; ; i++)
{
const AVCodecHWConfig *config = avcodec_get_hw_config(*Codec, i);
if (!config)
break;
const AVCodecHWConfig *config = avcodec_get_hw_config(*Codec, i);
if (!config)
break;

if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) &&
(config->device_type == AV_HWDEVICE_TYPE_CUDA))
if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) &&
(config->device_type == AV_HWDEVICE_TYPE_CUDA))
{
AVCodec *codec = avcodec_find_decoder_by_name(name.toLocal8Bit());
if (codec)
{
QString name = QString((*Codec)->name) + "_cuvid";
if (name == "mpeg2video_cuvid")
name = "mpeg2_cuvid";
AVCodec *codec = avcodec_find_decoder_by_name(name.toLocal8Bit());
if (codec)
{
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC supports decoding %1").arg(desc));
*Codec = codec;
decoder->CodecMap()->freeCodecContext(Stream);
*Context = decoder->CodecMap()->getCodecContext(Stream, *Codec);
return success;
}
break;
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC supports decoding %1").arg(desc));
*Codec = codec;
decoder->CodecMap()->freeCodecContext(Stream);
*Context = decoder->CodecMap()->getCodecContext(Stream, *Codec);
return success;
}
break;
}
}

LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC does NOT support %1").arg(desc));
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to find decoder '%1'").arg(name));
return failure;
}

Expand Down Expand Up @@ -498,10 +505,23 @@ bool MythNVDECContext::MythNVDECCaps::Supports(cudaVideoCodec Codec, cudaVideoCh
uint Depth, int Width, int Height)
{
uint mblocks = static_cast<uint>((Width * Height) / 256);
return (Codec == m_codec) && (Format == m_format) && (Depth == m_depth) &&
(m_maximum.width() >= Width) && (m_maximum.height() >= Height) &&
(m_minimum.width() <= Width) && (m_minimum.height() <= Height) &&
(m_macroBlocks >= mblocks);

LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
QString("Trying to match: Codec %1 Format %2 Depth %3 Width %4 Height %5 MBs %6")
.arg(Codec).arg(Format).arg(Depth).arg(Width).arg(Height).arg(mblocks));
LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
QString("to this profile: Codec %1 Format %2 Depth %3 Width %4<->%5 Height %6<->%7 MBs %8")
.arg(m_codec).arg(m_format).arg(m_depth)
.arg(m_minimum.width()).arg(m_maximum.width())
.arg(m_minimum.height()).arg(m_maximum.height()).arg(m_macroBlocks));

bool result = (Codec == m_codec) && (Format == m_format) && (Depth == m_depth) &&
(m_maximum.width() >= Width) && (m_maximum.height() >= Height) &&
(m_minimum.width() <= Width) && (m_minimum.height() <= Height) &&
(m_macroBlocks >= mblocks);

LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("%1 Match").arg(result ? "" : "NO"));
return result;
}

bool MythNVDECContext::HaveNVDEC(void)
Expand All @@ -525,9 +545,9 @@ bool MythNVDECContext::HaveNVDEC(void)
LOG(VB_GENERAL, LOG_INFO, LOC + "Supported/available NVDEC decoders:");
for (auto profile : profiles)
{
LOG(VB_GENERAL, LOG_INFO, LOC +
MythCodecContext::GetProfileDescription(profile.m_profile,profile.m_maximum,
profile.m_type, profile.m_depth + 8));
QString desc = MythCodecContext::GetProfileDescription(profile.m_profile,profile.m_maximum,
profile.m_type, profile.m_depth + 8);
LOG(VB_GENERAL, LOG_INFO, LOC + desc + QString(" MBs: %1").arg(profile.m_macroBlocks));
}
}
}
Expand Down
43 changes: 28 additions & 15 deletions mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,25 +333,38 @@ int MythVAAPIContext::InitialiseContext(AVCodecContext *Context)
// MPEG2 on Ironlake where it seems to return I420 labelled as NV12. I420 is
// buggy on Sandybridge (stride?) and produces a mixture of I420/NV12 frames
// for H.264 on Ironlake.
int format = VA_FOURCC_NV12;
QString vendor = interop->GetVendor();
if (vendor.contains("ironlake", Qt::CaseInsensitive))
if (CODEC_IS_MPEG(Context->codec_id))
format = VA_FOURCC_I420;
// This may need extending for AMD etc

if (format != VA_FOURCC_NV12)
QString vendor = interop->GetVendor();
// Intel NUC
if (vendor.contains("iHD", Qt::CaseInsensitive) && vendor.contains("Intel", Qt::CaseInsensitive))
{
vaapi_frames_ctx->attributes = nullptr;
vaapi_frames_ctx->nb_attributes = 0;
}
// i965 series
else
{
auto vaapiid = static_cast<MythCodecID>(kCodec_MPEG1_VAAPI + (mpeg_version(Context->codec_id) - 1));
LOG(VB_GENERAL, LOG_INFO, LOC + QString("Forcing surface format for %1 and %2 with driver '%3'")
.arg(toString(vaapiid)).arg(MythOpenGLInterop::TypeToString(type)).arg(vendor));
int format = VA_FOURCC_NV12;
if (vendor.contains("ironlake", Qt::CaseInsensitive))
if (CODEC_IS_MPEG(Context->codec_id))
format = VA_FOURCC_I420;

if (format != VA_FOURCC_NV12)
{
auto vaapiid = static_cast<MythCodecID>(kCodec_MPEG1_VAAPI + (mpeg_version(Context->codec_id) - 1));
LOG(VB_GENERAL, LOG_INFO, LOC + QString("Forcing surface format for %1 and %2 with driver '%3'")
.arg(toString(vaapiid)).arg(MythOpenGLInterop::TypeToString(type)).arg(vendor));
}

VASurfaceAttrib prefs[3] = {
{ VASurfaceAttribPixelFormat, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { format } } },
{ VASurfaceAttribUsageHint, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_USAGE_HINT_DISPLAY } } },
{ VASurfaceAttribMemoryType, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_MEM_TYPE_VA} } } };
vaapi_frames_ctx->attributes = prefs;
vaapi_frames_ctx->nb_attributes = 3;
}

VASurfaceAttrib prefs[3] = {
{ VASurfaceAttribPixelFormat, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { format } } },
{ VASurfaceAttribUsageHint, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_USAGE_HINT_DISPLAY } } },
{ VASurfaceAttribMemoryType, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_MEM_TYPE_VA} } } };
vaapi_frames_ctx->attributes = prefs;
vaapi_frames_ctx->nb_attributes = 3;
hw_frames_ctx->sw_format = FramesFormat(Context->sw_pix_fmt);
int referenceframes = AvFormatDecoder::GetMaxReferenceFrames(Context);
hw_frames_ctx->initial_pool_size = static_cast<int>(VideoBuffers::GetNumBuffers(FMT_VAAPI, referenceframes, true));
Expand Down
3 changes: 3 additions & 0 deletions mythtv/libs/libmythtv/libmythtv.pro
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,13 @@ using_frontend {
HEADERS += opengl/mythopenglvideoshaders.h
HEADERS += opengl/mythopenglinterop.h
HEADERS += opengl/mythvideotexture.h
HEADERS += opengl/mythopengltonemap.h
HEADERS += opengl/mythopenglcomputeshaders.h
SOURCES += opengl/mythopenglvideo.cpp
SOURCES += opengl/mythvideooutopengl.cpp
SOURCES += opengl/mythopenglinterop.cpp
SOURCES += opengl/mythvideotexture.cpp
SOURCES += opengl/mythopengltonemap.cpp

using_vaapi {
HEADERS += opengl/mythvaapiinterop.h opengl/mythvaapiglxinterop.h
Expand Down
116 changes: 116 additions & 0 deletions mythtv/libs/libmythtv/opengl/mythopenglcomputeshaders.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#ifndef MYTHOPENGLCOMPUTESHADERS_H
#define MYTHOPENGLCOMPUTESHADERS_H

#include <QString>

// This is a work in progress
// - assumes either NV12 or YV12
// - will probably break for rectangular and OES textures?
// - assumes HLG to Rec.709
// - various other hardcoded assumptions/settings

static const QString GLSL430Tonemap =
"#extension GL_ARB_compute_shader : enable\n"
"#extension GL_ARB_shader_storage_buffer_object : enable\n"
"#extension GL_ARB_shader_image_load_store : enable\n"
"layout(std430, binding=0) buffer FrameGlobals {\n"
" highp vec2 m_running;\n"
" uint m_frameMean;\n"
" uint m_frameMax;\n"
" uint m_wgCounter;\n"
"};\n"
"layout(rgba16f) uniform highp writeonly image2D m_texture;\n"
"#ifdef UNSIGNED\n"
"#define sampler2D usampler2D\n"
"#endif\n"
"uniform highp sampler2D texture0;\n"
"uniform highp sampler2D texture1;\n"
"#ifdef YV12\n"
"uniform highp sampler2D texture2;\n"
"#endif\n"
"uniform highp mat4 m_colourMatrix;\n"
"uniform highp mat4 m_primaryMatrix;\n"
"layout(local_size_x = 8, local_size_y = 8) in;\n"
"shared uint m_workGroupMean;\n"
"shared uint m_workGroupMax;\n"
"vec3 hable(vec3 H) {\n"
" return (H * (0.150000 * H + vec3(0.050000)) + vec3(0.004000)) / \n"
" (H * (0.150000 * H + vec3(0.500000)) + vec3(0.060000)) - vec3(0.066667);\n"
"}\n"
"void main() {\n"
" uint numWorkGroups = gl_NumWorkGroups.x * gl_NumWorkGroups.y;\n"
" highp vec2 coord = (vec2(gl_GlobalInvocationID) + vec2(0.5, 0.5)) / vec2(gl_NumWorkGroups * gl_WorkGroupSize);\n"
" highp vec4 pixel = vec4(texture(texture0, coord).r,\n"
"#ifdef YV12\n"
" texture(texture1, coord).r,\n"
" texture(texture2, coord).r,\n"
"#else\n"
" texture(texture1, coord).rg,\n"
"#endif\n"
" 1.0);\n"
"#ifdef UNSIGNED\n"
" pixel /= vec4(65535.0, 65535.0, 65535.0, 1.0);\n"
"#endif\n"
" pixel *= m_colourMatrix;\n"
" pixel = clamp(pixel, 0.0, 1.0);\n"
" pixel.rgb = mix(vec3(4.0) * pixel.rgb * pixel.rgb,\n"
" exp((pixel.rgb - vec3(0.559911)) * vec3(1.0/0.178833)) +\n"
" vec3(0.284669), lessThan(vec3(0.5), pixel.rgb));\n"
" pixel.rgb *= vec3(0.506970 * pow(dot(vec3(0.2627, 0.6780, 0.0593), pixel.rgb), 0.200000));\n"

" int channel = 0;\n"
" if (pixel.g > pixel.r) channel = 1;\n"
" if (pixel.b > pixel[channel]) channel = 2;\n"
" float channelmax = pixel[channel];\n"
" highp float mean = 0.25;\n"
" highp float peak = 10.0;\n"
" if (m_running.y > 0.0) {\n"
" mean = max(0.001, m_running.x);\n"
" peak = max(1.000, m_running.y);\n"
" }\n"

" m_workGroupMean = 0;\n"
" m_workGroupMax = 0;\n"
" barrier();\n"
" atomicAdd(m_workGroupMean, uint(log(max(channelmax, 0.001)) * 400.0));\n"
" atomicMax(m_workGroupMax, uint(channelmax * 10000.0));\n"
" memoryBarrierShared();\n"
" barrier();\n"

" if (gl_LocalInvocationIndex == 0) {\n"
" atomicAdd(m_frameMean, uint(m_workGroupMean / uint(gl_WorkGroupSize.x * gl_WorkGroupSize.y)));\n"
" atomicMax(m_frameMax, m_workGroupMax);\n"
" memoryBarrierBuffer();\n"
" }\n"
" barrier();\n"

" if (gl_LocalInvocationIndex == 0 && atomicAdd(m_wgCounter, 1) == (numWorkGroups - 1)) {\n"
" highp vec2 current = vec2(exp(float(m_frameMean) / (float(numWorkGroups) * 400.0)),\n"
" float(m_frameMax) / 10000.0);\n"
" if (m_running.y == 0.0) m_running = current;\n"
" m_running += 0.05 * (current - m_running);\n"
" float weight = smoothstep(1.266422, 2.302585, abs(log(current.x / m_running.x)));\n"
" m_running = mix(m_running, current, weight);\n"
" m_wgCounter = 0;\n"
" m_frameMean = 0;\n"
" m_frameMax = 0;\n"
" memoryBarrierBuffer();\n"
" }\n"

" vec3 colour = pixel.rgb;\n"
" float slope = min(1.000000, 0.25 / mean);\n"
" colour *= slope;\n"
" peak *= slope;\n"
" colour = hable(max(vec3(0.0), colour)) / hable(vec3(peak)).x;\n"
" colour = min(colour, vec3(1.0));\n"
" vec3 linear = pixel.rgb * (colour[channel] / channelmax);\n"
" float coeff = max(colour[channel] - 0.180000, 1e-6) / max(colour[channel], 1.0);\n"
" coeff = 0.750000 * pow(coeff, 1.500000);\n"
" pixel.rgb = mix(linear, colour, coeff);\n"

// BT2020 to Rec709
" pixel = m_primaryMatrix * clamp(pixel, 0.0, 1.0);\n"
" imageStore(m_texture, ivec2(gl_GlobalInvocationID), vec4(pow(pixel.rgb, vec3(1.0 / 2.2)), 1.0));\n"
"}\n";

#endif // MYTHOPENGLCOMPUTESHADERS_H
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
5 changes: 2 additions & 3 deletions mythtv/programs/mythtranscode/external/replex/replex.c
Original file line number Diff line number Diff line change
Expand Up @@ -2660,9 +2660,8 @@ int main(int argc, char **argv)
return 0;
}

void LogPrintLine( uint64_t mask, LogLevel_t level, const char *file, int line,
const char *function, int fromQString,
const char *format, ... )
void LogPrintLineC( uint64_t mask, LogLevel_t level, const char *file, int line,
const char *function, const char *format, ... )
{
va_list arguments;

Expand Down
6 changes: 3 additions & 3 deletions mythtv/programs/programs-libs.pro
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ using_hdhomerun:LIBS += -lhdhomerun
using_taglib: LIBS += $$CONFIG_TAGLIB_LIBS

!using_libexiv2_external {
LIBS += -L../../external/libexiv2 -lmythexiv2-0.28
freebsd: LIBS += -lexpat -lprocstat
darwin: LIBS += -lexpat -liconv -lz
LIBS += -L../../external/libexiv2 -lmythexiv2-0.28 -lexpat
freebsd: LIBS += -lprocstat
darwin: LIBS += -liconv -lz
}

win32 {
Expand Down