mirror of
https://github.com/vdr-projects/vdr.git
synced 2025-03-01 10:50:46 +00:00
VDR developer version 1.7.32 is now available at ftp://ftp.tvdr.de/vdr/Developer/vdr-1.7.32.tar.bz2 A 'diff' against the previous version is available at ftp://ftp.tvdr.de/vdr/Developer/vdr-1.7.31-1.7.32.diff MD5 checksums: 068ba78fd427694dcc480fe3b2d07148 vdr-1.7.32.tar.bz2 222f1e9b4d4edaa6fe57286409614cc7 vdr-1.7.31-1.7.32.diff WARNING: ======== This is a developer version. Even though I use it in my productive environment. I strongly recommend that you only use it under controlled conditions and for testing and debugging. The main focus of this version is on an improved frame detection code, and improvements to the cutting process. When cutting a recording, VDR now removes any "dangling" TS packets from the beginning of an editing sequence and pulls in any "pending" TS packets at the end of a sequence. It also fixes all timestamps and continuity counters. However, while the results look much better now in, for instance, Kaffeine, the TT S2-6400 still shows some video artifacts at the editing points, and the Mac video player sometimes totally chokes on edited material. I did spend a lot of time trying to find out what could still be wrong here, but couldn't come up with any new ideas. So I think it's now time to invite others to test this new cutting code, read the source code and try to find out what's still going wrong here. Maybe (hopefully ;-) it's just some stupid little error... ;-) From the HISTORY file: - Pressing the Play key during normal live viewing mode now opens the Recordings menu if there is no "last viewed" recording (thanks to Alexander Wenzel). The same behavior has been implemented for the Blue key in the main menu. - cIoThrottle::Engaged() is now also checked in cRemoveDeletedRecordingsThread::Action(), to suspend removing deleted recordings in case this is necessary to make room for new, ongoing recordings (suggested by Udo Richter). - The cThread constructor now has an additional boolean parameter that can be set to true to have this thread run at a lower priority. Plugin authors that use low priority threads may want to use this instead of the calls to SetPriority(19) and SetIOPriority(7). The priority of a thread ("low" or "high") is now logged when the thread starts. - Changed DTV_DVBT2_PLP_ID to DTV_STREAM_ID in dvbdevice.c to adapt to an incompatible change in DVB API 5.8 (reported by Derek Kelly). Removed the meanwhile obsolete definition of FE_CAN_TURBO_FEC. - Fixed some compiler warnings under gcc version 4.7.1. - Fixed setting the video format in the dvbhdffdevice (thanks to Torsten Lang). - Fixed 'make install' to not overwrite existing configuration files (thanks to Peter Münster). - Added including the Make.global and Make.config files to the dvbdhffdevice's libhdffcmd/Makefile. - Added options to build a 32-bit version of VDR on a 64-bit machine to Make.config.template. - Fixed handling VPS timers in case the running status of an event goes to '1' (not running) and later goes to '4' (running). - If a frame position in the 'marks' file of a recording doesn't point to an I-frame, it will now be shifted towards the next I-frame, either up or down, whichever is closer (suggested by Udo Richter). - Fixed a possible memory leak in SI::StructureLoop::getNextAsPointer() (reported by Sundararaj Reel). - Fixed handling timers in case an event is modified and "phased out" while the timer is recording. - Improved frame detection by parsing just far enough into the MPEG-4 NAL units to get the necessary information about frames and slices. - The initial syncing of the frame detector is now done immediately after the first complete GOP has been seen. This makes recordings and especially pausing live video start up to twice as fast as before. - Updated the Romanian OSD texts (thanks to Lucian Muresan). - Fixed handling the very last entry in a recording index. - The return type of cMarks::Add() has been changed to void, since due to the sorting of the list of marks the returned pointer might have pointed to a totally different mark. Besides, the return value was never actually used. - Improved editing TS recordings by + stripping dangling TS packets from the beginning of a sequence + including pending TS packets at the end of a sequence + fixing all timestamps and continuity counters + generating editing marks for the edited version in such a way that each cutting point is marked by an "end" and "begin" mark with the same offset + no longer generating an editing mark at the "end" of the edited recording (this was actually generated at the beginning of the last GOP, so that a subsequent edit would have cut off the last GOP) + no longer generating any editing marks if the edited recording results on just one single sequence + ignoring pairs of editing marks that are placed at exactly the same position of a recording when actually cutting the recording + not doing anything if the editing marks in place would result in the edited version being the same as the original recording - Editing marks can now be placed directly on top of each other, in which case they simply mark a position, but have no effect on the actual cutting process. - When positioned at an offset where two (or more) editing marks are placed on top of each other, the '4' key moves the first one of them to the left, while the '6' key moves the last one of them to the right. The '7' and '9' key handle multiple marks at the same place as if it were one single mark. - Modified editing marks are now written to disk whenever the replay progress display gets hidden (thanks to Christoph Haubrich).
870 lines
24 KiB
C
870 lines
24 KiB
C
/*
|
|
* timers.c: Timer handling
|
|
*
|
|
* See the main source file 'vdr.c' for copyright information and
|
|
* how to reach the author.
|
|
*
|
|
* $Id: timers.c 2.14 2012/10/16 08:22:39 kls Exp $
|
|
*/
|
|
|
|
#include "timers.h"
|
|
#include <ctype.h>
|
|
#include "channels.h"
|
|
#include "device.h"
|
|
#include "i18n.h"
|
|
#include "libsi/si.h"
|
|
#include "recording.h"
|
|
#include "remote.h"
|
|
#include "status.h"
|
|
|
|
#define VFAT_MAX_FILENAME 40 // same as MAX_SUBTITLE_LENGTH in recording.c
|
|
|
|
// IMPORTANT NOTE: in the 'sscanf()' calls there is a blank after the '%d'
|
|
// format characters in order to allow any number of blanks after a numeric
|
|
// value!
|
|
|
|
// --- cTimer ----------------------------------------------------------------
|
|
|
|
cTimer::cTimer(bool Instant, bool Pause, cChannel *Channel)
|
|
{
|
|
startTime = stopTime = 0;
|
|
lastSetEvent = 0;
|
|
deferred = 0;
|
|
recording = pending = inVpsMargin = false;
|
|
flags = tfNone;
|
|
*file = 0;
|
|
aux = NULL;
|
|
event = NULL;
|
|
if (Instant)
|
|
SetFlags(tfActive | tfInstant);
|
|
channel = Channel ? Channel : Channels.GetByNumber(cDevice::CurrentChannel());
|
|
time_t t = time(NULL);
|
|
struct tm tm_r;
|
|
struct tm *now = localtime_r(&t, &tm_r);
|
|
day = SetTime(t, 0);
|
|
weekdays = 0;
|
|
start = now->tm_hour * 100 + now->tm_min;
|
|
stop = 0;
|
|
if (!Setup.InstantRecordTime && channel) {
|
|
cSchedulesLock SchedulesLock;
|
|
if (const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock)) {
|
|
if (const cSchedule *Schedule = Schedules->GetSchedule(channel)) {
|
|
if (const cEvent *Event = Schedule->GetPresentEvent()) {
|
|
time_t tstart = Event->StartTime();
|
|
time_t tstop = Event->EndTime();
|
|
if (Event->Vps() && Setup.UseVps) {
|
|
SetFlags(tfVps);
|
|
tstart = Event->Vps();
|
|
}
|
|
else {
|
|
tstop += Setup.MarginStop * 60;
|
|
tstart -= Setup.MarginStart * 60;
|
|
}
|
|
day = SetTime(tstart, 0);
|
|
struct tm *time = localtime_r(&tstart, &tm_r);
|
|
start = time->tm_hour * 100 + time->tm_min;
|
|
time = localtime_r(&tstop, &tm_r);
|
|
stop = time->tm_hour * 100 + time->tm_min;
|
|
SetEvent(Event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!stop) {
|
|
stop = now->tm_hour * 60 + now->tm_min + (Setup.InstantRecordTime ? Setup.InstantRecordTime : DEFINSTRECTIME);
|
|
stop = (stop / 60) * 100 + (stop % 60);
|
|
}
|
|
if (stop >= 2400)
|
|
stop -= 2400;
|
|
priority = Pause ? Setup.PausePriority : Setup.DefaultPriority;
|
|
lifetime = Pause ? Setup.PauseLifetime : Setup.DefaultLifetime;
|
|
if (Instant && channel)
|
|
snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name());
|
|
if (VfatFileSystem && (Utf8StrLen(file) > VFAT_MAX_FILENAME)) {
|
|
dsyslog("timer file name too long for VFAT file system: '%s'", file);
|
|
file[Utf8SymChars(file, VFAT_MAX_FILENAME)] = 0;
|
|
dsyslog("timer file name truncated to '%s'", file);
|
|
}
|
|
}
|
|
|
|
cTimer::cTimer(const cEvent *Event)
|
|
{
|
|
startTime = stopTime = 0;
|
|
lastSetEvent = 0;
|
|
deferred = 0;
|
|
recording = pending = inVpsMargin = false;
|
|
flags = tfActive;
|
|
*file = 0;
|
|
aux = NULL;
|
|
event = NULL;
|
|
if (Event->Vps() && Setup.UseVps)
|
|
SetFlags(tfVps);
|
|
channel = Channels.GetByChannelID(Event->ChannelID(), true);
|
|
time_t tstart = (flags & tfVps) ? Event->Vps() : Event->StartTime();
|
|
time_t tstop = tstart + Event->Duration();
|
|
if (!(HasFlags(tfVps))) {
|
|
tstop += Setup.MarginStop * 60;
|
|
tstart -= Setup.MarginStart * 60;
|
|
}
|
|
struct tm tm_r;
|
|
struct tm *time = localtime_r(&tstart, &tm_r);
|
|
day = SetTime(tstart, 0);
|
|
weekdays = 0;
|
|
start = time->tm_hour * 100 + time->tm_min;
|
|
time = localtime_r(&tstop, &tm_r);
|
|
stop = time->tm_hour * 100 + time->tm_min;
|
|
if (stop >= 2400)
|
|
stop -= 2400;
|
|
priority = Setup.DefaultPriority;
|
|
lifetime = Setup.DefaultLifetime;
|
|
const char *Title = Event->Title();
|
|
if (!isempty(Title))
|
|
Utf8Strn0Cpy(file, Event->Title(), sizeof(file));
|
|
if (VfatFileSystem && (Utf8StrLen(file) > VFAT_MAX_FILENAME)) {
|
|
dsyslog("timer file name too long for VFAT file system: '%s'", file);
|
|
file[Utf8SymChars(file, VFAT_MAX_FILENAME)] = 0;
|
|
dsyslog("timer file name truncated to '%s'", file);
|
|
}
|
|
SetEvent(Event);
|
|
}
|
|
|
|
cTimer::cTimer(const cTimer &Timer)
|
|
{
|
|
channel = NULL;
|
|
aux = NULL;
|
|
event = NULL;
|
|
flags = tfNone;
|
|
*this = Timer;
|
|
}
|
|
|
|
cTimer::~cTimer()
|
|
{
|
|
free(aux);
|
|
}
|
|
|
|
cTimer& cTimer::operator= (const cTimer &Timer)
|
|
{
|
|
if (&Timer != this) {
|
|
uint OldFlags = flags & tfRecording;
|
|
startTime = Timer.startTime;
|
|
stopTime = Timer.stopTime;
|
|
lastSetEvent = 0;
|
|
deferred = 0;
|
|
recording = Timer.recording;
|
|
pending = Timer.pending;
|
|
inVpsMargin = Timer.inVpsMargin;
|
|
flags = Timer.flags | OldFlags;
|
|
channel = Timer.channel;
|
|
day = Timer.day;
|
|
weekdays = Timer.weekdays;
|
|
start = Timer.start;
|
|
stop = Timer.stop;
|
|
priority = Timer.priority;
|
|
lifetime = Timer.lifetime;
|
|
strncpy(file, Timer.file, sizeof(file));
|
|
free(aux);
|
|
aux = Timer.aux ? strdup(Timer.aux) : NULL;
|
|
event = NULL;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
int cTimer::Compare(const cListObject &ListObject) const
|
|
{
|
|
const cTimer *ti = (const cTimer *)&ListObject;
|
|
time_t t1 = StartTime();
|
|
time_t t2 = ti->StartTime();
|
|
int r = t1 - t2;
|
|
if (r == 0)
|
|
r = ti->priority - priority;
|
|
return r;
|
|
}
|
|
|
|
cString cTimer::ToText(bool UseChannelID) const
|
|
{
|
|
strreplace(file, ':', '|');
|
|
cString buffer = cString::sprintf("%u:%s:%s:%04d:%04d:%d:%d:%s:%s\n", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, weekdays, true), start, stop, priority, lifetime, file, aux ? aux : "");
|
|
strreplace(file, '|', ':');
|
|
return buffer;
|
|
}
|
|
|
|
cString cTimer::ToDescr(void) const
|
|
{
|
|
return cString::sprintf("%d (%d %04d-%04d %s'%s')", Index() + 1, Channel()->Number(), start, stop, HasFlags(tfVps) ? "VPS " : "", file);
|
|
}
|
|
|
|
int cTimer::TimeToInt(int t)
|
|
{
|
|
return (t / 100 * 60 + t % 100) * 60;
|
|
}
|
|
|
|
bool cTimer::ParseDay(const char *s, time_t &Day, int &WeekDays)
|
|
{
|
|
// possible formats are:
|
|
// 19
|
|
// 2005-03-19
|
|
// MTWTFSS
|
|
// MTWTFSS@19
|
|
// MTWTFSS@2005-03-19
|
|
|
|
Day = 0;
|
|
WeekDays = 0;
|
|
s = skipspace(s);
|
|
if (!*s)
|
|
return false;
|
|
const char *a = strchr(s, '@');
|
|
const char *d = a ? a + 1 : isdigit(*s) ? s : NULL;
|
|
if (d) {
|
|
if (strlen(d) == 10) {
|
|
struct tm tm_r;
|
|
if (3 == sscanf(d, "%d-%d-%d", &tm_r.tm_year, &tm_r.tm_mon, &tm_r.tm_mday)) {
|
|
tm_r.tm_year -= 1900;
|
|
tm_r.tm_mon--;
|
|
tm_r.tm_hour = tm_r.tm_min = tm_r.tm_sec = 0;
|
|
tm_r.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
Day = mktime(&tm_r);
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else {
|
|
// handle "day of month" for compatibility with older versions:
|
|
char *tail = NULL;
|
|
int day = strtol(d, &tail, 10);
|
|
if (tail && *tail || day < 1 || day > 31)
|
|
return false;
|
|
time_t t = time(NULL);
|
|
int DaysToCheck = 61; // 61 to handle months with 31/30/31
|
|
for (int i = -1; i <= DaysToCheck; i++) {
|
|
time_t t0 = IncDay(t, i);
|
|
if (GetMDay(t0) == day) {
|
|
Day = SetTime(t0, 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (a || !isdigit(*s)) {
|
|
if ((a && a - s == 7) || strlen(s) == 7) {
|
|
for (const char *p = s + 6; p >= s; p--) {
|
|
WeekDays <<= 1;
|
|
WeekDays |= (*p != '-');
|
|
}
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
cString cTimer::PrintDay(time_t Day, int WeekDays, bool SingleByteChars)
|
|
{
|
|
#define DAYBUFFERSIZE 64
|
|
char buffer[DAYBUFFERSIZE];
|
|
char *b = buffer;
|
|
if (WeekDays) {
|
|
// TRANSLATORS: the first character of each weekday, beginning with monday
|
|
const char *w = trNOOP("MTWTFSS");
|
|
if (!SingleByteChars)
|
|
w = tr(w);
|
|
while (*w) {
|
|
int sl = Utf8CharLen(w);
|
|
if (WeekDays & 1) {
|
|
for (int i = 0; i < sl; i++)
|
|
b[i] = w[i];
|
|
b += sl;
|
|
}
|
|
else
|
|
*b++ = '-';
|
|
WeekDays >>= 1;
|
|
w += sl;
|
|
}
|
|
if (Day)
|
|
*b++ = '@';
|
|
}
|
|
if (Day) {
|
|
struct tm tm_r;
|
|
localtime_r(&Day, &tm_r);
|
|
b += strftime(b, DAYBUFFERSIZE - (b - buffer), "%Y-%m-%d", &tm_r);
|
|
}
|
|
*b = 0;
|
|
return buffer;
|
|
}
|
|
|
|
cString cTimer::PrintFirstDay(void) const
|
|
{
|
|
if (weekdays) {
|
|
cString s = PrintDay(day, weekdays, true);
|
|
if (strlen(s) == 18)
|
|
return *s + 8;
|
|
}
|
|
return ""; // not NULL, so the caller can always use the result
|
|
}
|
|
|
|
bool cTimer::Parse(const char *s)
|
|
{
|
|
char *channelbuffer = NULL;
|
|
char *daybuffer = NULL;
|
|
char *filebuffer = NULL;
|
|
free(aux);
|
|
aux = NULL;
|
|
//XXX Apparently sscanf() doesn't work correctly if the last %a argument
|
|
//XXX results in an empty string (this first occurred when the EIT gathering
|
|
//XXX was put into a separate thread - don't know why this happens...
|
|
//XXX As a cure we copy the original string and add a blank.
|
|
//XXX If anybody can shed some light on why sscanf() failes here, I'd love
|
|
//XXX to hear about that!
|
|
char *s2 = NULL;
|
|
int l2 = strlen(s);
|
|
while (l2 > 0 && isspace(s[l2 - 1]))
|
|
l2--;
|
|
if (s[l2 - 1] == ':') {
|
|
s2 = MALLOC(char, l2 + 3);
|
|
strcat(strn0cpy(s2, s, l2 + 1), " \n");
|
|
s = s2;
|
|
}
|
|
bool result = false;
|
|
if (8 <= sscanf(s, "%u :%a[^:]:%a[^:]:%d :%d :%d :%d :%a[^:\n]:%a[^\n]", &flags, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &aux)) {
|
|
ClrFlags(tfRecording);
|
|
if (aux && !*skipspace(aux)) {
|
|
free(aux);
|
|
aux = NULL;
|
|
}
|
|
//TODO add more plausibility checks
|
|
result = ParseDay(daybuffer, day, weekdays);
|
|
if (VfatFileSystem) {
|
|
char *p = strrchr(filebuffer, FOLDERDELIMCHAR);
|
|
if (p)
|
|
p++;
|
|
else
|
|
p = filebuffer;
|
|
if (Utf8StrLen(p) > VFAT_MAX_FILENAME) {
|
|
dsyslog("timer file name too long for VFAT file system: '%s'", p);
|
|
p[Utf8SymChars(p, VFAT_MAX_FILENAME)] = 0;
|
|
dsyslog("timer file name truncated to '%s'", p);
|
|
}
|
|
}
|
|
Utf8Strn0Cpy(file, filebuffer, sizeof(file));
|
|
strreplace(file, '|', ':');
|
|
if (isnumber(channelbuffer))
|
|
channel = Channels.GetByNumber(atoi(channelbuffer));
|
|
else
|
|
channel = Channels.GetByChannelID(tChannelID::FromString(channelbuffer), true, true);
|
|
if (!channel) {
|
|
esyslog("ERROR: channel %s not defined", channelbuffer);
|
|
result = false;
|
|
}
|
|
}
|
|
free(channelbuffer);
|
|
free(daybuffer);
|
|
free(filebuffer);
|
|
free(s2);
|
|
return result;
|
|
}
|
|
|
|
bool cTimer::Save(FILE *f)
|
|
{
|
|
return fprintf(f, "%s", *ToText(true)) > 0;
|
|
}
|
|
|
|
bool cTimer::IsSingleEvent(void) const
|
|
{
|
|
return !weekdays;
|
|
}
|
|
|
|
int cTimer::GetMDay(time_t t)
|
|
{
|
|
struct tm tm_r;
|
|
return localtime_r(&t, &tm_r)->tm_mday;
|
|
}
|
|
|
|
int cTimer::GetWDay(time_t t)
|
|
{
|
|
struct tm tm_r;
|
|
int weekday = localtime_r(&t, &tm_r)->tm_wday;
|
|
return weekday == 0 ? 6 : weekday - 1; // we start with Monday==0!
|
|
}
|
|
|
|
bool cTimer::DayMatches(time_t t) const
|
|
{
|
|
return IsSingleEvent() ? SetTime(t, 0) == day : (weekdays & (1 << GetWDay(t))) != 0;
|
|
}
|
|
|
|
time_t cTimer::IncDay(time_t t, int Days)
|
|
{
|
|
struct tm tm_r;
|
|
tm tm = *localtime_r(&t, &tm_r);
|
|
tm.tm_mday += Days; // now tm_mday may be out of its valid range
|
|
int h = tm.tm_hour; // save original hour to compensate for DST change
|
|
tm.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
t = mktime(&tm); // normalize all values
|
|
tm.tm_hour = h; // compensate for DST change
|
|
return mktime(&tm); // calculate final result
|
|
}
|
|
|
|
time_t cTimer::SetTime(time_t t, int SecondsFromMidnight)
|
|
{
|
|
struct tm tm_r;
|
|
tm tm = *localtime_r(&t, &tm_r);
|
|
tm.tm_hour = SecondsFromMidnight / 3600;
|
|
tm.tm_min = (SecondsFromMidnight % 3600) / 60;
|
|
tm.tm_sec = SecondsFromMidnight % 60;
|
|
tm.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
return mktime(&tm);
|
|
}
|
|
|
|
void cTimer::SetFile(const char *File)
|
|
{
|
|
if (!isempty(File))
|
|
Utf8Strn0Cpy(file, File, sizeof(file));
|
|
}
|
|
|
|
#define EITPRESENTFOLLOWINGRATE 10 // max. seconds between two occurrences of the "EIT present/following table for the actual multiplex" (2s by the standard, using some more for safety)
|
|
|
|
bool cTimer::Matches(time_t t, bool Directly, int Margin) const
|
|
{
|
|
startTime = stopTime = 0;
|
|
if (t == 0)
|
|
t = time(NULL);
|
|
|
|
int begin = TimeToInt(start); // seconds from midnight
|
|
int length = TimeToInt(stop) - begin;
|
|
if (length < 0)
|
|
length += SECSINDAY;
|
|
|
|
if (IsSingleEvent()) {
|
|
startTime = SetTime(day, begin);
|
|
stopTime = startTime + length;
|
|
}
|
|
else {
|
|
for (int i = -1; i <= 7; i++) {
|
|
time_t t0 = IncDay(day ? max(day, t) : t, i);
|
|
if (DayMatches(t0)) {
|
|
time_t a = SetTime(t0, begin);
|
|
time_t b = a + length;
|
|
if ((!day || a >= day) && t < b) {
|
|
startTime = a;
|
|
stopTime = b;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!startTime)
|
|
startTime = IncDay(t, 7); // just to have something that's more than a week in the future
|
|
else if (!Directly && (t > startTime || t > day + SECSINDAY + 3600)) // +3600 in case of DST change
|
|
day = 0;
|
|
}
|
|
|
|
if (t < deferred)
|
|
return false;
|
|
deferred = 0;
|
|
|
|
if (HasFlags(tfActive)) {
|
|
if (HasFlags(tfVps) && event && event->Vps()) {
|
|
if (Margin || !Directly) {
|
|
startTime = event->StartTime();
|
|
stopTime = event->EndTime();
|
|
if (!Margin) { // this is an actual check
|
|
if (event->Schedule()->PresentSeenWithin(EITPRESENTFOLLOWINGRATE)) { // VPS control can only work with up-to-date events...
|
|
if (event->StartTime() > 0) // checks for "phased out" events
|
|
return event->IsRunning(true);
|
|
}
|
|
return startTime <= t && t < stopTime; // ...otherwise we fall back to normal timer handling
|
|
}
|
|
}
|
|
}
|
|
return startTime <= t + Margin && t < stopTime; // must stop *before* stopTime to allow adjacent timers
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#define FULLMATCH 1000
|
|
|
|
int cTimer::Matches(const cEvent *Event, int *Overlap) const
|
|
{
|
|
// Overlap is the percentage of the Event's duration that is covered by
|
|
// this timer (based on FULLMATCH for finer granularity than just 100).
|
|
// To make sure a VPS timer can be distinguished from a plain 100% overlap,
|
|
// it gets an additional 100 added, and a VPS event that is actually running
|
|
// gets 200 added to the FULLMATCH.
|
|
if (HasFlags(tfActive) && channel->GetChannelID() == Event->ChannelID()) {
|
|
bool UseVps = HasFlags(tfVps) && Event->Vps();
|
|
Matches(UseVps ? Event->Vps() : Event->StartTime(), true);
|
|
int overlap = 0;
|
|
if (UseVps)
|
|
overlap = (startTime == Event->Vps()) ? FULLMATCH + (Event->IsRunning() ? 200 : 100) : 0;
|
|
if (!overlap) {
|
|
if (startTime <= Event->StartTime() && Event->EndTime() <= stopTime)
|
|
overlap = FULLMATCH;
|
|
else if (stopTime <= Event->StartTime() || Event->EndTime() <= startTime)
|
|
overlap = 0;
|
|
else
|
|
overlap = (min(stopTime, Event->EndTime()) - max(startTime, Event->StartTime())) * FULLMATCH / max(Event->Duration(), 1);
|
|
}
|
|
startTime = stopTime = 0;
|
|
if (Overlap)
|
|
*Overlap = overlap;
|
|
if (UseVps)
|
|
return overlap > FULLMATCH ? tmFull : tmNone;
|
|
return overlap >= FULLMATCH ? tmFull : overlap > 0 ? tmPartial : tmNone;
|
|
}
|
|
return tmNone;
|
|
}
|
|
|
|
#define EXPIRELATENCY 60 // seconds (just in case there's a short glitch in the VPS signal)
|
|
|
|
bool cTimer::Expired(void) const
|
|
{
|
|
return IsSingleEvent() && !Recording() && StopTime() + EXPIRELATENCY <= time(NULL) && (!HasFlags(tfVps) || !event || !event->Vps());
|
|
}
|
|
|
|
time_t cTimer::StartTime(void) const
|
|
{
|
|
if (!startTime)
|
|
Matches();
|
|
return startTime;
|
|
}
|
|
|
|
time_t cTimer::StopTime(void) const
|
|
{
|
|
if (!stopTime)
|
|
Matches();
|
|
return stopTime;
|
|
}
|
|
|
|
#define EPGLIMITBEFORE (1 * 3600) // Time in seconds before a timer's start time and
|
|
#define EPGLIMITAFTER (1 * 3600) // after its stop time within which EPG events will be taken into consideration.
|
|
|
|
void cTimer::SetEventFromSchedule(const cSchedules *Schedules)
|
|
{
|
|
cSchedulesLock SchedulesLock;
|
|
if (!Schedules) {
|
|
lastSetEvent = 0; // forces setting the event, even if the schedule hasn't been modified
|
|
if (!(Schedules = cSchedules::Schedules(SchedulesLock)))
|
|
return;
|
|
}
|
|
const cSchedule *Schedule = Schedules->GetSchedule(Channel());
|
|
if (Schedule && Schedule->Events()->First()) {
|
|
time_t now = time(NULL);
|
|
if (!lastSetEvent || Schedule->Modified() >= lastSetEvent) {
|
|
lastSetEvent = now;
|
|
const cEvent *Event = NULL;
|
|
if (HasFlags(tfVps) && Schedule->Events()->First()->Vps()) {
|
|
if (event && event->StartTime() > 0) { // checks for "phased out" events
|
|
if (Recording())
|
|
return; // let the recording end first
|
|
if (now <= event->EndTime() || Matches(0, true))
|
|
return; // stay with the old event until the timer has completely expired
|
|
}
|
|
// VPS timers only match if their start time exactly matches the event's VPS time:
|
|
for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
|
|
if (e->StartTime() && e->RunningStatus() != SI::RunningStatusNotRunning) { // skip outdated events
|
|
int overlap = 0;
|
|
Matches(e, &overlap);
|
|
if (overlap > FULLMATCH) {
|
|
Event = e;
|
|
break; // take the first matching event
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Normal timers match the event they have the most overlap with:
|
|
int Overlap = 0;
|
|
// Set up the time frame within which to check events:
|
|
Matches(0, true);
|
|
time_t TimeFrameBegin = StartTime() - EPGLIMITBEFORE;
|
|
time_t TimeFrameEnd = StopTime() + EPGLIMITAFTER;
|
|
for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
|
|
if (e->EndTime() < TimeFrameBegin)
|
|
continue; // skip events way before the timer starts
|
|
if (e->StartTime() > TimeFrameEnd)
|
|
break; // the rest is way after the timer ends
|
|
int overlap = 0;
|
|
Matches(e, &overlap);
|
|
if (overlap && overlap >= Overlap) {
|
|
if (Event && overlap == Overlap && e->Duration() <= Event->Duration())
|
|
continue; // if overlap is the same, we take the longer event
|
|
Overlap = overlap;
|
|
Event = e;
|
|
}
|
|
}
|
|
}
|
|
SetEvent(Event);
|
|
}
|
|
}
|
|
}
|
|
|
|
void cTimer::SetEvent(const cEvent *Event)
|
|
{
|
|
if (event != Event) { //XXX TODO check event data, too???
|
|
if (Event)
|
|
isyslog("timer %s set to event %s", *ToDescr(), *Event->ToDescr());
|
|
else
|
|
isyslog("timer %s set to no event", *ToDescr());
|
|
event = Event;
|
|
}
|
|
}
|
|
|
|
void cTimer::SetRecording(bool Recording)
|
|
{
|
|
recording = Recording;
|
|
if (recording)
|
|
SetFlags(tfRecording);
|
|
else
|
|
ClrFlags(tfRecording);
|
|
isyslog("timer %s %s", *ToDescr(), recording ? "start" : "stop");
|
|
}
|
|
|
|
void cTimer::SetPending(bool Pending)
|
|
{
|
|
pending = Pending;
|
|
}
|
|
|
|
void cTimer::SetInVpsMargin(bool InVpsMargin)
|
|
{
|
|
if (InVpsMargin && !inVpsMargin)
|
|
isyslog("timer %s entered VPS margin", *ToDescr());
|
|
inVpsMargin = InVpsMargin;
|
|
}
|
|
|
|
void cTimer::SetDay(time_t Day)
|
|
{
|
|
day = Day;
|
|
}
|
|
|
|
void cTimer::SetWeekDays(int WeekDays)
|
|
{
|
|
weekdays = WeekDays;
|
|
}
|
|
|
|
void cTimer::SetStart(int Start)
|
|
{
|
|
start = Start;
|
|
}
|
|
|
|
void cTimer::SetStop(int Stop)
|
|
{
|
|
stop = Stop;
|
|
}
|
|
|
|
void cTimer::SetPriority(int Priority)
|
|
{
|
|
priority = Priority;
|
|
}
|
|
|
|
void cTimer::SetLifetime(int Lifetime)
|
|
{
|
|
lifetime = Lifetime;
|
|
}
|
|
|
|
void cTimer::SetAux(const char *Aux)
|
|
{
|
|
free(aux);
|
|
aux = strdup(Aux);
|
|
}
|
|
|
|
void cTimer::SetDeferred(int Seconds)
|
|
{
|
|
deferred = time(NULL) + Seconds;
|
|
isyslog("timer %s deferred for %d seconds", *ToDescr(), Seconds);
|
|
}
|
|
|
|
void cTimer::SetFlags(uint Flags)
|
|
{
|
|
flags |= Flags;
|
|
}
|
|
|
|
void cTimer::ClrFlags(uint Flags)
|
|
{
|
|
flags &= ~Flags;
|
|
}
|
|
|
|
void cTimer::InvFlags(uint Flags)
|
|
{
|
|
flags ^= Flags;
|
|
}
|
|
|
|
bool cTimer::HasFlags(uint Flags) const
|
|
{
|
|
return (flags & Flags) == Flags;
|
|
}
|
|
|
|
void cTimer::Skip(void)
|
|
{
|
|
day = IncDay(SetTime(StartTime(), 0), 1);
|
|
startTime = 0;
|
|
SetEvent(NULL);
|
|
}
|
|
|
|
void cTimer::OnOff(void)
|
|
{
|
|
if (IsSingleEvent())
|
|
InvFlags(tfActive);
|
|
else if (day) {
|
|
day = 0;
|
|
ClrFlags(tfActive);
|
|
}
|
|
else if (HasFlags(tfActive))
|
|
Skip();
|
|
else
|
|
SetFlags(tfActive);
|
|
SetEvent(NULL);
|
|
Matches(); // refresh start and end time
|
|
}
|
|
|
|
// --- cTimers ---------------------------------------------------------------
|
|
|
|
cTimers Timers;
|
|
|
|
cTimers::cTimers(void)
|
|
{
|
|
state = 0;
|
|
beingEdited = 0;;
|
|
lastSetEvents = 0;
|
|
lastDeleteExpired = 0;
|
|
}
|
|
|
|
cTimer *cTimers::GetTimer(cTimer *Timer)
|
|
{
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->Channel() == Timer->Channel() &&
|
|
(ti->WeekDays() && ti->WeekDays() == Timer->WeekDays() || !ti->WeekDays() && ti->Day() == Timer->Day()) &&
|
|
ti->Start() == Timer->Start() &&
|
|
ti->Stop() == Timer->Stop())
|
|
return ti;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
cTimer *cTimers::GetMatch(time_t t)
|
|
{
|
|
static int LastPending = -1;
|
|
cTimer *t0 = NULL;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Recording() && ti->Matches(t)) {
|
|
if (ti->Pending()) {
|
|
if (ti->Index() > LastPending)
|
|
LastPending = ti->Index();
|
|
else
|
|
continue;
|
|
}
|
|
if (!t0 || ti->Priority() > t0->Priority())
|
|
t0 = ti;
|
|
}
|
|
}
|
|
if (!t0)
|
|
LastPending = -1;
|
|
return t0;
|
|
}
|
|
|
|
cTimer *cTimers::GetMatch(const cEvent *Event, int *Match)
|
|
{
|
|
cTimer *t = NULL;
|
|
int m = tmNone;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
int tm = ti->Matches(Event);
|
|
if (tm > m) {
|
|
t = ti;
|
|
m = tm;
|
|
if (m == tmFull)
|
|
break;
|
|
}
|
|
}
|
|
if (Match)
|
|
*Match = m;
|
|
return t;
|
|
}
|
|
|
|
cTimer *cTimers::GetNextActiveTimer(void)
|
|
{
|
|
cTimer *t0 = NULL;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
ti->Matches();
|
|
if ((ti->HasFlags(tfActive)) && (!t0 || ti->StopTime() > time(NULL) && ti->Compare(*t0) < 0))
|
|
t0 = ti;
|
|
}
|
|
return t0;
|
|
}
|
|
|
|
void cTimers::SetModified(void)
|
|
{
|
|
cStatus::MsgTimerChange(NULL, tcMod);
|
|
state++;
|
|
}
|
|
|
|
void cTimers::Add(cTimer *Timer, cTimer *After)
|
|
{
|
|
cConfig<cTimer>::Add(Timer, After);
|
|
cStatus::MsgTimerChange(Timer, tcAdd);
|
|
}
|
|
|
|
void cTimers::Ins(cTimer *Timer, cTimer *Before)
|
|
{
|
|
cConfig<cTimer>::Ins(Timer, Before);
|
|
cStatus::MsgTimerChange(Timer, tcAdd);
|
|
}
|
|
|
|
void cTimers::Del(cTimer *Timer, bool DeleteObject)
|
|
{
|
|
cStatus::MsgTimerChange(Timer, tcDel);
|
|
cConfig<cTimer>::Del(Timer, DeleteObject);
|
|
}
|
|
|
|
bool cTimers::Modified(int &State)
|
|
{
|
|
bool Result = state != State;
|
|
State = state;
|
|
return Result;
|
|
}
|
|
|
|
void cTimers::SetEvents(void)
|
|
{
|
|
if (time(NULL) - lastSetEvents < 5)
|
|
return;
|
|
cSchedulesLock SchedulesLock(false, 100);
|
|
const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
|
|
if (Schedules) {
|
|
if (!lastSetEvents || Schedules->Modified() >= lastSetEvents) {
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (cRemote::HasKeys())
|
|
return; // react immediately on user input
|
|
ti->SetEventFromSchedule(Schedules);
|
|
}
|
|
}
|
|
}
|
|
lastSetEvents = time(NULL);
|
|
}
|
|
|
|
void cTimers::DeleteExpired(void)
|
|
{
|
|
if (time(NULL) - lastDeleteExpired < 30)
|
|
return;
|
|
cTimer *ti = First();
|
|
while (ti) {
|
|
cTimer *next = Next(ti);
|
|
if (ti->Expired()) {
|
|
isyslog("deleting timer %s", *ti->ToDescr());
|
|
Del(ti);
|
|
SetModified();
|
|
}
|
|
ti = next;
|
|
}
|
|
lastDeleteExpired = time(NULL);
|
|
}
|
|
|
|
// --- cSortedTimers ---------------------------------------------------------
|
|
|
|
static int CompareTimers(const void *a, const void *b)
|
|
{
|
|
return (*(const cTimer **)a)->Compare(**(const cTimer **)b);
|
|
}
|
|
|
|
cSortedTimers::cSortedTimers(void)
|
|
:cVector<const cTimer *>(Timers.Count())
|
|
{
|
|
for (const cTimer *Timer = Timers.First(); Timer; Timer = Timers.Next(Timer))
|
|
Append(Timer);
|
|
Sort(CompareTimers);
|
|
}
|