diff --git a/HISTORY b/HISTORY index 3378b1ad..87510ec1 100644 --- a/HISTORY +++ b/HISTORY @@ -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). diff --git a/MANUAL b/MANUAL index beb7444c..c4d4391f 100644 --- a/MANUAL +++ b/MANUAL @@ -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 diff --git a/README.vps b/README.vps new file mode 100644 index 00000000..2aa19ce8 --- /dev/null +++ b/README.vps @@ -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... diff --git a/config.c b/config.c index 530da577..aaf376ab 100644 --- a/config.c +++ b/config.c @@ -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); diff --git a/config.h b/config.h index 34cad46f..12991262 100644 --- a/config.h +++ b/config.h @@ -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; diff --git a/epg.c b/epg.c index ce839751..be378c09 100644 --- a/epg.c +++ b/epg.c @@ -7,11 +7,12 @@ * Original version (as used in VDR before 1.3.0) written by * Robert Schneider and Rolf Hakenes . * - * $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 #include @@ -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--; } diff --git a/epg.h b/epg.h index 8b6067d1..81866fd3 100644 --- a/epg.h +++ b/epg.h @@ -7,7 +7,7 @@ * Original version (as used in VDR before 1.3.0) written by * Robert Schneider and Rolf Hakenes . * - * $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; diff --git a/i18n.c b/i18n.c index bdd88348..4d30fccc 100644 --- a/i18n.c +++ b/i18n.c @@ -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", diff --git a/menu.c b/menu.c index 187174af..7b70218c 100644 --- a/menu.c +++ b/menu.c @@ -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); diff --git a/menuitems.c b/menuitems.c index e6641541..49874f11 100644 --- a/menuitems.c +++ b/menuitems.c @@ -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) diff --git a/menuitems.h b/menuitems.h index f9b2ff30..7091740d 100644 --- a/menuitems.h +++ b/menuitems.h @@ -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; diff --git a/svdrp.c b/svdrp.c index f55ad557..5d35eae7 100644 --- a/svdrp.c +++ b/svdrp.c @@ -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 diff --git a/timers.c b/timers.c index 74f36391..2ec387a4 100644 --- a/timers.c +++ b/timers.c @@ -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 #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); + } + } +} diff --git a/timers.h b/timers.h index b70983c2..ce4bff60 100644 --- a/timers.h +++ b/timers.h @@ -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; diff --git a/vdr.5 b/vdr.5 index 7ced5507..e54c9f8e 100644 --- a/vdr.5 +++ b/vdr.5 @@ -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 diff --git a/vdr.c b/vdr.c index 4782df9b..2da8349c 100644 --- a/vdr.c +++ b/vdr.c @@ -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 @@ -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!"));