Implemented VPS controlled timers

This commit is contained in:
Klaus Schmidinger 2004-02-29 14:21:22 +01:00
parent 4063d72760
commit 198fcf437b
16 changed files with 526 additions and 81 deletions

26
HISTORY
View File

@ -2652,7 +2652,7 @@ Video Disk Recorder Revision History
actual CAM type as reported by the CAM. The 'ca.conf' file has been stripped
down to the values 0..4.
2004-02-23: Version 1.3.5
2004-02-29: Version 1.3.5
- Fixed reading the EPG preferred language parameter from 'setup.conf'.
- Fixed switching to a visible programme in case the current channel has neither
@ -2680,11 +2680,8 @@ Video Disk Recorder Revision History
channel won't interrupt an ongoing Transfer Mode.
- Added subtable ID and TSDT handling to 'libsi' (thanks to Marcel Wiesweg).
- Fixed some Russian OSD texts (thanks to Vyacheslav Dikonov).
- Added the 'running status' to the EPG events. This might lead to a VPS like
function for recording, but unfortunately not all stations handle this flag
correctly - and some (like RTL, for instance) even change the ID of the same
event randomly, making it impossible for a timer to be programmed on a ceartain
event rather than a specific time. Well, let's see where this leads us...
- Added the 'running status' to the EPG events. This is necessary for implementing
the VPS function for recording.
- Removed the obsolete 'present' and 'following' handling from the EPG data.
- The EPG data is now always kept sorted chronologically in the internal data
structures. This also means that any EPG data retrieved through the SVRDP
@ -2698,10 +2695,21 @@ Video Disk Recorder Revision History
still displayed in the "Schedule" menu (thanks to Jaakko Hyvätti).
- Added PDCDescriptor handling to 'libsi'.
- Implemented handling the VPS timestamps (aka "Programme Identification Label")
in preparation for full VPS support for timers (provided the tv stations
actually broadcast this information). Currently these are just displayed in
the event page if they exist and are different than the event's start time.
for full VPS support for timers (provided the tv stations actually broadcast
this information). The VPS time is displayed in the event info page if it exists
and is different than the event's start time.
- Extended the SVDRP command LSTE to allow limiting the listed data to a given
channel, the present or following events, or events at a given time (thanks to
Thomas Heiligenmann).
- Fixed a typo in libsi/si.h (thanks to Stéphane Esté-Gracias).
- Timers can now be set to use the VPS information to control recording a programme.
The new setup options "Recording/Use VPS" and "Recording/VPS margin", as well as
the "VPS" option in the individual timers, can be used to control this feature
(see MANUAL for details).
Note that this feature will certainly need a lot of testing before it can be
called "safe"!
- The "Schedule" and "What's on now/next?" menus now have an additional column
which displays information on whether there is a timer defined for an event,
whether an event has a VPS time that's different than its start time, and
whether an event is currently running (see MANUAL under "The "Schedule" Menu"
for details).

29
MANUAL
View File

@ -138,6 +138,16 @@ Version 1.2
The "Blue" button can be pressed to switch to the channel with the selected
programme.
The following markers in these menus give additional information about the
status of the events:
t there is a timer defined for this event which covers only part of the event
T there is a timer defined for this event which covers the entire event
V this event has a VPS time that's different than its start time
* this event is currently running (the validity of this marker depends on
whether there is currently a DVB card receiving the transponder this channel
is on).
* Selecting a Channel
There are four ways to select a channel:
@ -351,6 +361,14 @@ Version 1.2
would have a Day setting of "M-W----".
Start: The start time of the timer in hh:mm as 24 hour ("military") time.
Stop: The stop time of the timer.
VPS: Defines whether the timer shall use VPS (if available). If this
option is set to 'yes', the start time must exactly match the
programme's VPS time, otherwise nothing will be recorded. If VPS
is used, the stop time has no real meaning. However, it must be
different than the start time, and should correspond to the actual
stop time of the programme, just in case there is no real VPS data
available at the time of recording, so VDR has to fall back to
normal timer recording.
Priority: The Priority (0..99) is used to decide which timer shall be
started in case there are two or more timers with the exact same
start time. The first timer in the list with the highest Priority
@ -585,6 +603,17 @@ Version 1.2
no = don't use the 'Episode name'
yes = use it (and create subdirectories)
Use VPS = 0 Defines whether a timer that is created from an EPG entry
(by pressing the "Record" (red) button in the "Schedules"
or "What's on now/next?" menu) will automatically use VPS
if the event it is created for has a VPS time.
VPS margin = 120 Defines how many seconds before a VPS controlled timer is
scheduled to start, VDR will make sure that one of the DVB
devices is tuned to the transponder that timer shall record
from. This is necessary for the "Running Status" information
that is broadcast in the EPG data to be seen by VDR.
Mark instant recording = yes
Defines whether an "instant recording" (started by
pressing the "Red" button in the "VDR" menu) will be

130
README.vps Normal file
View File

@ -0,0 +1,130 @@
VPS (Video Programming Service)
===============================
Beginning with version 1.3.5 VDR supports the VPS method
of identifying programmes to record, and making sure they
are recorded in full length, even if they run longer than
initially specified or are shifted in time.
Of course, the main prerequisite for this to work is that
the broadcasters actually provide the necessary data. In
particular these are
- EPG data (well, obviously)
- the data for each event must contain the "Programme Identification Label"
descriptor, which contains the VPS timestamp for this programme
- the event data must provide and maintain the "Running Status" flag,
which indicates whether this programme is currently running or not.
Currently only the German "Öffentlich Rechtliche" tv stations provide
the necessary VPS data, so this will work only for stations like "Das Erste",
"ZDF" and the like.
Following is a step by step description of what happens for a VPS controlled
timer recording. First let's take a look at what the VDR user needs to do.
VPS as seen by the VDR user:
----------------------------
When the VDR user sets up a timer that shall be under VPS control, there
are only two things that need to be done:
1. Set the "Start" time to the actual VPS time as published in tv magazines.
Typically the VPS time is the same as the printed start time, unless
expliciltly specified otherwise. For instance, a tv magazine might print
20:15 Wetten, dass...?
(VPS = 20:14)
In this case the timer would need to be set to 20:14.
2. Set the "VPS" flag in the timer definition to "yes" in order to tell VDR
that this timer is to be handled under VPS control. This is no different
to old analog video recorders, where each timer has also had a separate
VPS flag.
If the user sets up a timer from the "Schedule" menu, things are even simpler.
If the setup option "Recording/Use VPS" is set to "yes", any timer that is
programmed from an event that has VPS information will automatically be set
up as a VPS timer.
IMPORTANT: In order for a recording to work under VPS control it is of
========== paramount importance that the start time is set to the actual
VPS time of that event, NOT some time a few minutes before the
event starts! If a timer is set to use VPS, and the time doesn't
match any event's VPS time, nothing will be recorded!
VPS as seen by VDR:
-------------------
The following things happen when VDR processes timers:
- VDR regularly scans the EPG data and assigns events to the timers (see
cTimers::SetEvents() in VDR/timers.c).
This can be seen in the log file as
timer 1 (15 1830-1900 'Neues') set to event 28.02.2004 18:30-18:59 (VPS: 28.02 18:30) 'neues'
- When a VPS timer is asked whether it matches (i.e. whether a recording shall
be started), it checks whether it has an event assigned to it, and whether
that event has a running status of "starts in a few seconds" or "running"
(see cTimer::Matches(time_t t, bool Directly) in VDR/timers.c). This allows
the recording process to catch the entire programme, even if it runs longer
than initially advertised. It also works if it runs shorter or gets shifted.
- When a VPS timer event is coming up (i.e. there are only a few minutes until
it starts, according to the related event data's start time - which may be
different than the VPS time!), VDR tunes a free DVB device to that transponder
(unless there is already a device tuned to that one) in order to make sure
that the event data (especially the "Running Status") will be up to date and
a change in the "Running status" flag will be seen immediately. This may
lead to the primary device being switched to a different channel if there
is no other free DVB device available. See the main program loop in VDR/vdr.c,
"Make sure VPS timers "see" their channel early enough:".
Problems:
---------
- In order for a VPS controlled timer to function properly, it needs to "see"
any changes in the running status of its event. This means that one of the
DVB devices needs to be tuned to the proper transponder some time before
the actual start time of the event. However, this may result in an other
timer (with lower priority) to be stopped, because it occupies the DVB device
and has it tuned to a different transponder.
See "// Make sure VPS timers "see" their channel early enough:" in VDR/vdr.c.
TODO:
Something needs to be done to prevent two timers from repeatedly switching
the device between channels in such a situation.
- If, for some reason, the driver doesn't deliver any more section data, a
VPS controlled timer will never see that the programme has started (or ended).
TODO:
Therefore some mechanism needs to be implemented that makes absolutely sure
we continuously receive at least the event data for the present event.
Caveats:
--------
Apparently VPS in digital broadcasting is still in an early state. Only few
tv stations actually support it, and other tv stations don't even handle the
"Running Status" correctly (which, by itself, would already be helpful, even
without VPS).
Here's a list of things that are apparently done wrong by the individual
stations:
- The German "Öffentlich Rechtliche" tv stations, although supporting VPS,
don't switch the "Running Status" of an upcoming broadcast to "starts in
a few seconds", but rather go directly from "unknown" or "not running" to
"running". This may result in a recording that misses the first few seconds
of the programme.
- The RTL group handles EPG events in a rather random way. They change event
IDs randomly, and switch the "Running Status" flag at times that are only
losely related to the actual events. For instance, if the "RTL aktuell"
programme starts at 18:45, they switch that event to "running" at about
18:43. Or, even worse, if "Wer wird Millionär?" runs until 21:15, they
switch the _next_ programme to running (which implicitly set "Wer wird
Millionär?" to "not running) at around 21:11 - so anybody using that
information to control recording would not see the end of that programme.
... more following as it comes up...

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: config.c 1.124 2004/02/21 15:05:40 kls Exp $
* $Id: config.c 1.125 2004/02/28 11:12:20 kls Exp $
*/
#include "config.h"
@ -272,6 +272,8 @@ cSetup::cSetup(void)
PausePriority = 10;
PauseLifetime = 1;
UseSubtitle = 1;
UseVps = 0;
VpsMargin = 120;
RecordingDirs = 1;
VideoFormat = 0;
UpdateChannels = 4;
@ -417,6 +419,8 @@ bool cSetup::Parse(const char *Name, const char *Value)
else if (!strcasecmp(Name, "PausePriority")) PausePriority = atoi(Value);
else if (!strcasecmp(Name, "PauseLifetime")) PauseLifetime = atoi(Value);
else if (!strcasecmp(Name, "UseSubtitle")) UseSubtitle = atoi(Value);
else if (!strcasecmp(Name, "UseVps")) UseVps = atoi(Value);
else if (!strcasecmp(Name, "VpsMargin")) VpsMargin = atoi(Value);
else if (!strcasecmp(Name, "RecordingDirs")) RecordingDirs = atoi(Value);
else if (!strcasecmp(Name, "VideoFormat")) VideoFormat = atoi(Value);
else if (!strcasecmp(Name, "UpdateChannels")) UpdateChannels = atoi(Value);
@ -469,6 +473,8 @@ bool cSetup::Save(void)
Store("PausePriority", PausePriority);
Store("PauseLifetime", PauseLifetime);
Store("UseSubtitle", UseSubtitle);
Store("UseVps", UseVps);
Store("VpsMargin", VpsMargin);
Store("RecordingDirs", RecordingDirs);
Store("VideoFormat", VideoFormat);
Store("UpdateChannels", UpdateChannels);

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: config.h 1.188 2004/02/21 15:04:53 kls Exp $
* $Id: config.h 1.189 2004/02/28 11:11:35 kls Exp $
*/
#ifndef __CONFIG_H
@ -228,6 +228,8 @@ public:
int DefaultPriority, DefaultLifetime;
int PausePriority, PauseLifetime;
int UseSubtitle;
int UseVps;
int VpsMargin;
int RecordingDirs;
int VideoFormat;
int UpdateChannels;

14
epg.c
View File

@ -7,11 +7,12 @@
* Original version (as used in VDR before 1.3.0) written by
* Robert Schneider <Robert.Schneider@web.de> and Rolf Hakenes <hakenes@hippomi.de>.
*
* $Id: epg.c 1.13 2004/02/22 14:41:37 kls Exp $
* $Id: epg.c 1.14 2004/02/29 13:48:34 kls Exp $
*/
#include "epg.h"
#include "libsi/si.h"
#include "timers.h"
#include <ctype.h>
#include <time.h>
@ -95,6 +96,15 @@ void cEvent::SetVps(time_t Vps)
vps = Vps;
}
bool cEvent::HasTimer(void) const
{
for (cTimer *t = Timers.First(); t; t = Timers.Next(t)) {
if (t->Event() == this)
return true;
}
return false;
}
const char *cEvent::GetDateString(void) const
{
static char buf[25];
@ -545,7 +555,7 @@ void cSchedule::Cleanup(time_t Time)
Event = events.Get(a);
if (!Event)
break;
if (Event->StartTime() + Event->Duration() + Setup.EPGLinger * 60 + 3600 < Time) { // adding one hour for safety
if (!Event->HasTimer() && Event->StartTime() + Event->Duration() + Setup.EPGLinger * 60 + 3600 < Time) { // adding one hour for safety
events.Del(Event);
a--;
}

3
epg.h
View File

@ -7,7 +7,7 @@
* Original version (as used in VDR before 1.3.0) written by
* Robert Schneider <Robert.Schneider@web.de> and Rolf Hakenes <hakenes@hippomi.de>.
*
* $Id: epg.h 1.10 2004/02/22 14:34:04 kls Exp $
* $Id: epg.h 1.11 2004/02/29 14:10:06 kls Exp $
*/
#ifndef __EPG_H
@ -51,6 +51,7 @@ public:
time_t StartTime(void) const { return startTime; }
int Duration(void) const { return duration; }
time_t Vps(void) const { return vps; }
bool HasTimer(void) const;
const char *GetDateString(void) const;
const char *GetTimeString(void) const;
const char *GetEndTimeString(void) const;

56
i18n.c
View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: i18n.c 1.148 2004/02/21 15:14:36 kls Exp $
* $Id: i18n.c 1.149 2004/02/28 11:13:27 kls Exp $
*
* Translations provided by:
*
@ -1542,6 +1542,24 @@ const tI18nPhrase Phrases[] = {
"Fi",
"ºÞÝÕæ",
},
{ "VPS",
"VPS",
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
},
{ "Priority",
"Priorität",
"Prioriteta",
@ -2805,6 +2823,42 @@ const tI18nPhrase Phrases[] = {
"Utilitzar el nom de l'episodi",
"³àãßßØàÞÒÐâì äÐÙÛë ßÞ íßØ×ÞÔÐÜ",
},
{ "Setup.Recording$Use VPS",
"VPS benutzen",
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
},
{ "Setup.Recording$VPS margin (s)",
"Zeitpuffer bei VPS (s)",
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
"",// TODO
},
{ "Setup.Recording$Mark instant recording",
"Direktaufzeichnung markieren",
"Oznaci direktno snemanje",

53
menu.c
View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: menu.c 1.292 2004/02/22 14:14:55 kls Exp $
* $Id: menu.c 1.293 2004/02/29 14:11:16 kls Exp $
*/
#include "menu.h"
@ -18,6 +18,7 @@
#include "cutter.h"
#include "eitscan.h"
#include "i18n.h"
#include "libsi/si.h"
#include "menuitems.h"
#include "plugin.h"
#include "recording.h"
@ -871,13 +872,14 @@ cMenuEditTimer::cMenuEditTimer(cTimer *Timer, bool New)
if (timer) {
data = *timer;
if (New)
data.active = 1;
data.SetFlags(tfActive);
channel = data.Channel()->Number();
Add(new cMenuEditBoolItem(tr("Active"), &data.active));
Add(new cMenuEditBitItem( tr("Active"), &data.flags, tfActive));
Add(new cMenuEditChanItem(tr("Channel"), &channel));
Add(new cMenuEditDayItem( tr("Day"), &data.day));
Add(new cMenuEditTimeItem(tr("Start"), &data.start));
Add(new cMenuEditTimeItem(tr("Stop"), &data.stop));
Add(new cMenuEditBitItem( tr("VPS"), &data.flags, tfVps));
Add(new cMenuEditIntItem( tr("Priority"), &data.priority, 0, MAXPRIORITY));
Add(new cMenuEditIntItem( tr("Lifetime"), &data.lifetime, 0, MAXLIFETIME));
Add(new cMenuEditStrItem( tr("File"), data.file, sizeof(data.file), tr(FileNameChars)));
@ -926,13 +928,13 @@ eOSState cMenuEditTimer::ProcessKey(eKeys Key)
if (timer) {
if (memcmp(timer, &data, sizeof(data)) != 0) {
*timer = data;
if (timer->active)
timer->active = 1; // allows external programs to mark active timers with values > 1 and recognize if the user has modified them
if (timer->HasFlags(tfActive))
timer->ClrFlags(~tfAll); // allows external programs to mark active timers with values > 0xFFFF and recognize if the user has modified them
}
if (addIfConfirmed)
Timers.Add(timer);
Timers.Save();
isyslog("timer %d %s (%s)", timer->Index() + 1, addIfConfirmed ? "added" : "modified", timer->active ? "active" : "inactive");
isyslog("timer %d %s (%s)", timer->Index() + 1, addIfConfirmed ? "added" : "modified", timer->HasFlags(tfActive) ? "active" : "inactive");
addIfConfirmed = false;
}
}
@ -976,7 +978,7 @@ void cMenuTimerItem::Set(void)
{
char *buffer = NULL;
asprintf(&buffer, "%c\t%d\t%s\t%02d:%02d\t%02d:%02d\t%s",
!timer->Active() ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>',
!(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>',
timer->Channel()->Number(),
timer->PrintDay(timer->Day()),
timer->Start() / 100,
@ -1041,7 +1043,7 @@ eOSState cMenuTimers::OnOff(void)
if (timer->FirstDay())
isyslog("timer %d first day set to %s", timer->Index() + 1, timer->PrintFirstDay());
else
isyslog("timer %d %sactivated", timer->Index() + 1, timer->Active() ? "" : "de");
isyslog("timer %d %sactivated", timer->Index() + 1, timer->HasFlags(tfActive) ? "" : "de");
Timers.Save();
}
return osContinue;
@ -1211,7 +1213,11 @@ cMenuWhatsOnItem::cMenuWhatsOnItem(const cEvent *Event, cChannel *Channel)
event = Event;
channel = Channel;
char *buffer = NULL;
asprintf(&buffer, "%d\t%.*s\t%.*s\t%s", channel->Number(), 6, channel->Name(), 5, event->GetTimeString(), event->Title());
int TimerMatch;
char t = Timers.GetMatch(Event, &TimerMatch) ? (TimerMatch == tmFull) ? 'T' : 't' : ' ';
char v = event->Vps() && (event->Vps() - event->StartTime()) ? 'V' : ' ';
char r = event->RunningStatus() > SI::RunningStatusNotRunning ? '*' : ' ';
asprintf(&buffer, "%d\t%.*s\t%.*s\t%c%c%c\t%s", channel->Number(), 6, channel->Name(), 5, event->GetTimeString(), t, v, r, event->Title());
SetText(buffer, false);
}
@ -1235,7 +1241,7 @@ int cMenuWhatsOn::currentChannel = 0;
const cEvent *cMenuWhatsOn::scheduleEvent = NULL;
cMenuWhatsOn::cMenuWhatsOn(const cSchedules *Schedules, bool Now, int CurrentChannelNr)
:cOsdMenu(Now ? tr("What's on now?") : tr("What's on next?"), CHNUMWIDTH, 7, 6)
:cOsdMenu(Now ? tr("What's on now?") : tr("What's on next?"), CHNUMWIDTH, 7, 6, 4)
{
for (cChannel *Channel = Channels.First(); Channel; Channel = Channels.Next(Channel)) {
if (!Channel->GroupSep()) {
@ -1325,7 +1331,11 @@ cMenuScheduleItem::cMenuScheduleItem(const cEvent *Event)
{
event = Event;
char *buffer = NULL;
asprintf(&buffer, "%.*s\t%.*s\t%s", 5, event->GetDateString(), 5, event->GetTimeString(), event->Title());
int TimerMatch;
char t = Timers.GetMatch(Event, &TimerMatch) ? (TimerMatch == tmFull) ? 'T' : 't' : ' ';
char v = event->Vps() && (event->Vps() - event->StartTime()) ? 'V' : ' ';
char r = event->RunningStatus() > SI::RunningStatusNotRunning ? '*' : ' ';
asprintf(&buffer, "%.*s\t%.*s\t%c%c%c\t%s", 5, event->GetDateString(), 5, event->GetTimeString(), t, v, r, event->Title());
SetText(buffer, false);
}
@ -1347,7 +1357,7 @@ public:
};
cMenuSchedule::cMenuSchedule(void)
:cOsdMenu("", 6, 6)
:cOsdMenu("", 6, 6, 4)
{
now = next = false;
otherChannel = 0;
@ -2294,6 +2304,8 @@ cMenuSetupRecord::cMenuSetupRecord(void)
Add(new cMenuEditIntItem( tr("Setup.Recording$Pause priority"), &data.PausePriority, 0, MAXPRIORITY));
Add(new cMenuEditIntItem( tr("Setup.Recording$Pause lifetime (d)"), &data.PauseLifetime, 0, MAXLIFETIME));
Add(new cMenuEditBoolItem(tr("Setup.Recording$Use episode name"), &data.UseSubtitle));
Add(new cMenuEditBoolItem(tr("Setup.Recording$Use VPS"), &data.UseVps));
Add(new cMenuEditIntItem( tr("Setup.Recording$VPS margin (s)"), &data.VpsMargin, 0));
Add(new cMenuEditBoolItem(tr("Setup.Recording$Mark instant recording"), &data.MarkInstantRecord));
Add(new cMenuEditStrItem( tr("Setup.Recording$Name instant recording"), data.NameInstantRecord, sizeof(data.NameInstantRecord), tr(FileNameChars)));
Add(new cMenuEditIntItem( tr("Setup.Recording$Instant rec. time (min)"), &data.InstantRecordTime, 1, MAXINSTANTRECTIME));
@ -3056,11 +3068,12 @@ cRecordControl::cRecordControl(cDevice *Device, cTimer *Timer, bool Pause)
}
timer->SetPending(true);
timer->SetRecording(true);
event = timer->Event();
const char *Title = NULL;
const char *Subtitle = NULL;
const char *Summary = NULL;
if (GetEvent()) {
if (event || GetEvent()) {
Title = event->Title();
Subtitle = event->ShortText();
Summary = event->Description();
@ -3115,7 +3128,7 @@ cRecordControl::~cRecordControl()
bool cRecordControl::GetEvent(void)
{
const cChannel *channel = timer->Channel();
time_t Time = timer->Active() == taActInst ? timer->StartTime() + INSTANT_REC_EPG_LOOKAHEAD : timer->StartTime() + (timer->StopTime() - timer->StartTime()) / 2;
time_t Time = timer->HasFlags(tfInstant) ? timer->StartTime() + INSTANT_REC_EPG_LOOKAHEAD : timer->StartTime() + (timer->StopTime() - timer->StartTime()) / 2;
for (int seconds = 0; seconds <= MAXWAIT4EPGINFO; seconds++) {
{
cSchedulesLock SchedulesLock;
@ -3187,12 +3200,14 @@ bool cRecordControls::Start(cTimer *Timer, bool Pause)
cThread::EmergencyExit(true);
return false;
}
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (!RecordControls[i]) {
RecordControls[i] = new cRecordControl(device, Timer, Pause);
return true;
if (!Timer || Timer->Matches()) {
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (!RecordControls[i]) {
RecordControls[i] = new cRecordControl(device, Timer, Pause);
return true;
}
}
}
}
}
else if (!Timer || (Timer->Priority() >= Setup.PrimaryLimit && !Timer->Pending()))
isyslog("no free DVB device to record channel %d!", ch);

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: menuitems.c 1.14 2004/01/25 15:40:55 kls Exp $
* $Id: menuitems.c 1.15 2004/02/24 12:38:43 kls Exp $
*/
#include "menuitems.h"
@ -113,6 +113,23 @@ void cMenuEditBoolItem::Set(void)
SetValue(buf);
}
// --- cMenuEditBitItem ------------------------------------------------------
cMenuEditBitItem::cMenuEditBitItem(const char *Name, int *Value, int Mask, const char *FalseString, const char *TrueString)
:cMenuEditBoolItem(Name, &bit, FalseString, TrueString)
{
value = Value;
bit = (*value & Mask) != 0;
mask = Mask;
Set();
}
void cMenuEditBitItem::Set(void)
{
*value = bit ? *value | mask : *value & ~mask;
cMenuEditBoolItem::Set();
}
// --- cMenuEditNumItem ------------------------------------------------------
cMenuEditNumItem::cMenuEditNumItem(const char *Name, char *Value, int Length, bool Blind)

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: menuitems.h 1.5 2003/01/12 15:06:23 kls Exp $
* $Id: menuitems.h 1.6 2004/02/24 11:55:14 kls Exp $
*/
#ifndef __MENUITEMS_H
@ -42,6 +42,16 @@ public:
cMenuEditBoolItem(const char *Name, int *Value, const char *FalseString = NULL, const char *TrueString = NULL);
};
class cMenuEditBitItem : public cMenuEditBoolItem {
protected:
int *value;
int bit;
int mask;
virtual void Set(void);
public:
cMenuEditBitItem(const char *Name, int *Value, int Mask, const char *FalseString = NULL, const char *TrueString = NULL);
};
class cMenuEditNumItem : public cMenuEditItem {
protected:
char *value;

View File

@ -10,7 +10,7 @@
* and interact with the Video Disk Recorder - or write a full featured
* graphical interface that sits on top of an SVDRP connection.
*
* $Id: svdrp.c 1.60 2004/02/22 15:31:23 kls Exp $
* $Id: svdrp.c 1.61 2004/02/24 12:24:43 kls Exp $
*/
#include "svdrp.h"
@ -905,16 +905,16 @@ void cSVDRP::CmdMODT(const char *Option)
if (timer) {
cTimer t = *timer;
if (strcasecmp(tail, "ON") == 0)
t.SetActive(taActive);
t.SetFlags(tfActive);
else if (strcasecmp(tail, "OFF") == 0)
t.SetActive(taInactive);
t.ClrFlags(tfActive);
else if (!t.Parse(tail)) {
Reply(501, "Error in timer settings");
return;
}
*timer = t;
Timers.Save();
isyslog("timer %d modified (%s)", timer->Index() + 1, timer->Active() ? "active" : "inactive");
isyslog("timer %d modified (%s)", timer->Index() + 1, timer->HasFlags(tfActive) ? "active" : "inactive");
Reply(250, "%d %s", timer->Index() + 1, timer->ToText());
}
else

161
timers.c
View File

@ -4,13 +4,14 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: timers.c 1.9 2004/02/13 15:37:49 kls Exp $
* $Id: timers.c 1.10 2004/02/29 14:20:48 kls Exp $
*/
#include "timers.h"
#include <ctype.h>
#include "channels.h"
#include "i18n.h"
#include "libsi/si.h"
// 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
@ -23,8 +24,10 @@ char *cTimer::buffer = NULL;
cTimer::cTimer(bool Instant, bool Pause)
{
startTime = stopTime = 0;
recording = pending = false;
active = Instant ? taActInst : taInactive;
recording = pending = inVpsMargin = false;
flags = tfNone;
if (Instant)
SetFlags(tfActive | tfInstant);
channel = Channels.GetByNumber(cDevice::CurrentChannel());
time_t t = time(NULL);
struct tm tm_r;
@ -40,6 +43,7 @@ cTimer::cTimer(bool Instant, bool Pause)
*file = 0;
firstday = 0;
summary = NULL;
event = NULL;
if (Instant && channel)
snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name());
}
@ -47,12 +51,17 @@ cTimer::cTimer(bool Instant, bool Pause)
cTimer::cTimer(const cEvent *Event)
{
startTime = stopTime = 0;
recording = pending = false;
active = true;
recording = pending = inVpsMargin = false;
flags = tfActive;
if (Event->Vps() && Setup.UseVps)
SetFlags(tfVps);
channel = Channels.GetByChannelID(Event->ChannelID(), true);
time_t tstart = Event->StartTime();
time_t tstop = tstart + Event->Duration() + Setup.MarginStop * 60;
tstart -= Setup.MarginStart * 60;
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 = time->tm_mday;
@ -69,6 +78,7 @@ cTimer::cTimer(const cEvent *Event)
strn0cpy(file, Event->Title(), sizeof(file));
firstday = 0;
summary = NULL;
event = Event;
}
cTimer::~cTimer()
@ -81,6 +91,7 @@ cTimer& cTimer::operator= (const cTimer &Timer)
memcpy(this, &Timer, sizeof(*this));
if (summary)
summary = strdup(summary);
event = NULL;
return *this;
}
@ -97,7 +108,7 @@ const char *cTimer::ToText(bool UseChannelID)
free(buffer);
strreplace(file, ':', '|');
strreplace(summary, '\n', '|');
asprintf(&buffer, "%d:%s:%s:%04d:%04d:%d:%d:%s:%s\n", active, UseChannelID ? Channel()->GetChannelID().ToString() : itoa(Channel()->Number()), PrintDay(day, firstday), start, stop, priority, lifetime, file, summary ? summary : "");
asprintf(&buffer, "%d:%s:%s:%04d:%04d:%d:%d:%s:%s\n", flags, UseChannelID ? Channel()->GetChannelID().ToString() : itoa(Channel()->Number()), PrintDay(day, firstday), start, stop, priority, lifetime, file, summary ? summary : "");
strreplace(summary, '|', '\n');
strreplace(file, '|', ':');
return buffer;
@ -205,7 +216,7 @@ bool cTimer::Parse(const char *s)
s = s2;
}
bool result = false;
if (8 <= sscanf(s, "%d :%a[^:]:%a[^:]:%d :%d :%d :%d :%a[^:\n]:%a[^\n]", &active, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &summary)) {
if (8 <= sscanf(s, "%d :%a[^:]:%a[^:]:%d :%d :%d :%d :%a[^:\n]:%a[^\n]", &flags, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &summary)) {
if (summary && !*skipspace(summary)) {
free(summary);
summary = NULL;
@ -290,7 +301,7 @@ char *cTimer::SetFile(const char *File)
return file;
}
bool cTimer::Matches(time_t t)
bool cTimer::Matches(time_t t, bool Directly)
{
startTime = stopTime = 0;
if (t == 0)
@ -316,9 +327,35 @@ bool cTimer::Matches(time_t t)
}
if (!startTime)
startTime = firstday; // just to have something that's more than a week in the future
else if (t > startTime || t > firstday + SECSINDAY + 3600) // +3600 in case of DST change
else if (!Directly && (t > startTime || t > firstday + SECSINDAY + 3600)) // +3600 in case of DST change
firstday = 0;
return active && startTime <= t && t < stopTime; // must stop *before* stopTime to allow adjacent timers
if (HasFlags(tfActive)) {
if (HasFlags(tfVps) && !Directly && event && event->Vps()) {
startTime = event->StartTime();
stopTime = startTime + event->Duration();
return event->RunningStatus() > SI::RunningStatusNotRunning;
}
return startTime <= t && t < stopTime; // must stop *before* stopTime to allow adjacent timers
}
return false;
}
int cTimer::Matches(const cEvent *Event)
{
if (channel->GetChannelID() == Event->ChannelID()) {
bool UseVps = HasFlags(tfVps) && Event->Vps();
time_t t1 = UseVps ? Event->Vps() : Event->StartTime();
time_t t2 = t1 + Event->Duration();
bool m1 = Matches(t1, true);
bool m2 = UseVps ? m1 : Matches(t2, true);
startTime = stopTime = 0;
if (m1 && m2)
return tmFull;
if (m1 || m2)
return tmPartial;
}
return tmNone;
}
time_t cTimer::StartTime(void)
@ -335,6 +372,19 @@ time_t cTimer::StopTime(void)
return stopTime;
}
void cTimer::SetEvent(const cEvent *Event)
{
if (event != Event) { //XXX TODO check event data, too???
if (Event) {
char vpsbuf[64] = "";
if (Event->Vps())
sprintf(vpsbuf, "(VPS: %s) ", Event->GetVpsString());
isyslog("timer %d (%d %04d-%04d '%s') set to event %s %s-%s %s'%s'", Index() + 1, Channel()->Number(), start, stop, file, Event->GetDateString(), Event->GetTimeString(), Event->GetEndTimeString(), vpsbuf, Event->Title());
}
event = Event;
}
}
void cTimer::SetRecording(bool Recording)
{
recording = Recording;
@ -346,28 +396,52 @@ void cTimer::SetPending(bool Pending)
pending = Pending;
}
void cTimer::SetActive(int Active)
void cTimer::SetInVpsMargin(bool InVpsMargin)
{
active = Active;
if (InVpsMargin && !inVpsMargin)
isyslog("timer %d (%d %04d-%04d '%s') entered VPS margin", Index() + 1, Channel()->Number(), start, stop, file);
inVpsMargin = InVpsMargin;
}
void cTimer::SetFlags(int Flags)
{
flags |= Flags;
}
void cTimer::ClrFlags(int Flags)
{
flags &= ~Flags;
}
void cTimer::InvFlags(int Flags)
{
flags ^= Flags;
}
bool cTimer::HasFlags(int Flags)
{
return (flags & Flags) == Flags;
}
void cTimer::Skip(void)
{
firstday = IncDay(SetTime(StartTime(), 0), 1);
event = NULL;
}
void cTimer::OnOff(void)
{
if (IsSingleEvent())
active = !active;
InvFlags(tfActive);
else if (firstday) {
firstday = 0;
active = false;
ClrFlags(tfActive);
}
else if (active)
else if (HasFlags(tfActive))
Skip();
else
active = true;
SetFlags(tfActive);
event = NULL;
Matches(); // refresh start and end time
}
@ -396,12 +470,59 @@ cTimer *cTimers::GetMatch(time_t t)
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)) {
if (ti->Active() && (!t0 || *ti < *t0))
if ((ti->HasFlags(tfActive)) && (!t0 || *ti < *t0))
t0 = ti;
}
return t0;
}
void cTimers::SetEvents(void)
{
cSchedulesLock SchedulesLock;
const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
if (Schedules) {
for (cTimer *ti = First(); ti; ti = Next(ti)) {
const cSchedule *Schedule = Schedules->GetSchedule(ti->Channel()->GetChannelID());
const cEvent *Event = NULL;
if (Schedule) {
//XXX what if the Schedule doesn't have any VPS???
const cEvent *e;
int Match = tmNone;
int i = 0;
while ((e = Schedule->GetEventNumber(i++)) != NULL) {
int m = ti->Matches(e);
if (m > Match) {
Match = m;
Event = e;
if (Match == tmFull)
break;
//XXX what if there's another event with the same VPS time???
}
}
}
ti->SetEvent(Event);
}
}
}

View File

@ -4,7 +4,7 @@
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: timers.h 1.6 2003/12/13 13:04:21 kls Exp $
* $Id: timers.h 1.7 2004/02/29 14:18:17 kls Exp $
*/
#ifndef __TIMERS_H
@ -15,19 +15,21 @@
#include "epg.h"
#include "tools.h"
enum eTimerActive { taInactive = 0,
taActive = 1,
taInstant = 2,
taActInst = (taActive | taInstant)
};
enum eTimerFlags { tfNone = 0x0000,
tfActive = 0x0001,
tfInstant = 0x0002,
tfVps = 0x0004,
tfAll = 0xFFFF,
};
enum eTimerMatch { tmNone, tmPartial, tmFull };
class cTimer : public cListObject {
friend class cMenuEditTimer;
private:
time_t startTime, stopTime;
static char *buffer;
bool recording, pending;
int active;
bool recording, pending, inVpsMargin;
int flags;
cChannel *channel;
int day;
int start;
@ -37,6 +39,7 @@ private:
char file[MaxFileName];
time_t firstday;
char *summary;
const cEvent *event;
public:
cTimer(bool Instant = false, bool Pause = false);
cTimer(const cEvent *Event);
@ -45,7 +48,8 @@ public:
virtual bool operator< (const cListObject &ListObject);
bool Recording(void) { return recording; }
bool Pending(void) { return pending; }
int Active(void) { return active; }
bool InVpsMargin(void) { return inVpsMargin; }
int Flags(void) { return flags; }
const cChannel *Channel(void) { return channel; }
int Day(void) { return day; }
int Start(void) { return start; }
@ -56,6 +60,7 @@ public:
time_t FirstDay(void) { return firstday; }
const char *Summary(void) { return summary; }
const char *ToText(bool UseChannelID = false);
const cEvent *Event(void) { return event; }
bool Parse(const char *s);
bool Save(FILE *f);
bool IsSingleEvent(void);
@ -65,12 +70,18 @@ public:
static time_t IncDay(time_t t, int Days);
static time_t SetTime(time_t t, int SecondsFromMidnight);
char *SetFile(const char *File);
bool Matches(time_t t = 0);
bool Matches(time_t t = 0, bool Directly = false);
int Matches(const cEvent *Event);
time_t StartTime(void);
time_t StopTime(void);
void SetEvent(const cEvent *Event);
void SetRecording(bool Recording);
void SetPending(bool Pending);
void SetActive(int Active);
void SetInVpsMargin(bool InVpsMargin);
void SetFlags(int Flags);
void ClrFlags(int Flags);
void InvFlags(int Flags);
bool HasFlags(int Flags);
void Skip(void);
void OnOff(void);
const char *PrintFirstDay(void);
@ -85,10 +96,12 @@ private:
public:
cTimer *GetTimer(cTimer *Timer);
cTimer *GetMatch(time_t t);
cTimer *GetMatch(const cEvent *Event, int *Match = NULL);
cTimer *GetNextActiveTimer(void);
int BeingEdited(void) { return beingEdited; }
void IncBeingEdited(void) { beingEdited++; }
void DecBeingEdited(void) { beingEdited--; }
void SetEvents(void);
};
extern cTimers Timers;

19
vdr.5
View File

@ -8,7 +8,7 @@
.\" License as specified in the file COPYING that comes with the
.\" vdr distribution.
.\"
.\" $Id: vdr.5 1.25 2004/02/22 13:18:48 kls Exp $
.\" $Id: vdr.5 1.26 2004/02/24 12:36:35 kls Exp $
.\"
.TH vdr 5 "1 Jun 2003" "1.2.0" "Video Disk Recorder Files"
.SH NAME
@ -196,12 +196,17 @@ The fields in a timer definition have the following meaning (from left
to right):
.TP
.B Status
Defines whether this timer is \fBinactive\fR (0) or \fBactive\fR (1).
The value 3 is used for instant recordings.
Values other than these can be used by external programs to mark active timers
and recognize if the user has modified them. When a user modifes an active
timer the \fBstatus\fR field will be explicitly set to '1' (or '0', respectively,
if the user deactivates the timer).
The individual bits in this field have the following meaning:
.TS
tab (@);
l l.
\fB1\fR@the timer is active (and will record if it hits)
\fB2\fR@this is an instant recording timer
\fB4\fR@this timer uses VPS
.TE
Bits other than these can be used by external programs to mark active timers
and recognize if the user has modified them. When a user modifies an active
timer, the upper 16 bits of this 32 bit parameter will be explicitly set to 0.
Note: in order to allow future extensibility, external programs using the
\fBstatus\fR parameter should only use the upper 16 bit of this 32 bit parameter

30
vdr.c
View File

@ -22,7 +22,7 @@
*
* The project's page is at http://www.cadsoft.de/vdr
*
* $Id: vdr.c 1.177 2004/02/15 14:29:30 kls Exp $
* $Id: vdr.c 1.178 2004/02/29 14:21:22 kls Exp $
*/
#include <getopt.h>
@ -483,6 +483,7 @@ int main(int argc, char *argv[])
int MaxLatencyTime = 0;
bool ForceShutdown = false;
bool UserShutdown = false;
bool TimerInVpsMargin = false;
while (!Interrupted) {
// Handle emergency exits:
@ -548,8 +549,15 @@ int main(int argc, char *argv[])
PreviousChannel[PreviousChannelIndex ^= 1] = LastChannel;
// Timers and Recordings:
if (!Timers.BeingEdited()) {
time_t Now = time(NULL); // must do both following calls with the exact same time!
static time_t LastSetEvents = 0;//XXX trigger by actual EPG data modification???
if (!Menu && time(NULL) - LastSetEvents > 5) {
Timers.SetEvents();
LastSetEvents = time(NULL);
}
time_t Now = time(NULL); // must do all following calls with the exact same time!
// Process ongoing recordings:
cRecordControls::Process(Now);
// Start new recordings:
cTimer *Timer = Timers.GetMatch(Now);
if (Timer) {
if (!cRecordControls::Start(Timer))
@ -557,6 +565,21 @@ int main(int argc, char *argv[])
else
LastTimerChannel = Timer->Channel()->Number();
}
// Make sure VPS timers "see" their channel early enough:
TimerInVpsMargin = false;
for (cTimer *Timer = Timers.First(); Timer; Timer = Timers.Next(Timer)) {
if (Timer->HasFlags(tfActive | tfVps) && !Timer->Recording() && !Timer->Pending() && Timer->Matches(Now + Setup.VpsMargin, true)) {
if (!Timer->InVpsMargin()) {
Timer->SetInVpsMargin(true);
TimerInVpsMargin = true;
//XXX if not primary device has TP???
LastTimerChannel = Timer->Channel()->Number();
cRecordControls::Start(Timer); // will only switch the device
}
}
else
Timer->SetInVpsMargin(false);
}
}
// CAM control:
if (!Menu && !Interface->IsOpen())
@ -758,7 +781,8 @@ int main(int argc, char *argv[])
}
}
if (!Menu) {
EITScanner.Process();
if (!TimerInVpsMargin)
EITScanner.Process();
if (!cCutter::Active() && cCutter::Ended()) {
if (cCutter::Error())
Interface->Error(tr("Editing process failed!"));