/* * eit.c: EIT section filter * * See the main source file 'vdr.c' for copyright information and * how to reach the author. * * Original version (as used in VDR before 1.3.0) written by * Robert Schneider and Rolf Hakenes . * Adapted to 'libsi' for VDR 1.3.0 by Marcel Wiesweg . * * $Id: eit.c 5.6 2022/12/23 09:47:23 kls Exp $ */ // The various ways in which broadcasters handle (or screw up) their EPG: // - Some use the same version for all tables, and use unique event ids, which are the same in // both the 0x5X and the 0x6X tables. And once an event has an id, it keeps it until it is // no longer in the tables. Those are the good guys! // - Some use separate versions for each table (0x50, 0x51, ...). // - Some broadcast tables 0x5X and 0x6X, but use different event ids for the same event in both // sets of tables, and sometimes even use different titles, short texts or descriptions. // - Some broadcast the full EPG only on one transponder (tables 0x6X), and on the actual transponder // they provide only the present/following information. // - Some have overlapping events, especially when they mess up daylight saving time. // - Some use all new event ids every time they update their tables. // So, to bring order to chaos, VDR does as follows: // - Completely ignore table 0x4F. // - Once a schedule has seen events from 0x5X, tables 0x6X are ignored for that schedule. // - When looking up an event in its schedule, the start time is used for tables 0x6X, and the // event id for tables 0x4E and 0x5X. #include "eit.h" #include #include "epg.h" #include "i18n.h" #include "libsi/section.h" #include "libsi/descriptor.h" #define VALID_TIME (31536000 * 2) // two years #define DBGEIT 0 // --- cEitTables ------------------------------------------------------------ cEitTables::cEitTables(void) { complete = false; tableStart = 0; tableEnd = 0; } bool cEitTables::Check(uchar TableId, uchar Version, int SectionNumber) { int ti = Index(TableId); return sectionSyncer[ti].Check(Version, SectionNumber); } bool cEitTables::Processed(uchar TableId, uchar LastTableId, int SectionNumber, int LastSectionNumber, int SegmentLastSectionNumber) { bool Result = false; int ti = Index(TableId); int LastIndex = Index(LastTableId); complete = false; if (sectionSyncer[ti].Processed(SectionNumber, LastSectionNumber, SegmentLastSectionNumber)) { Result = true; // the table with TableId is complete for (int i = 0; i <= LastIndex; i++) { if (!sectionSyncer[i].Complete()) return Result; } complete = true; // all tables have been processed } return Result; } // --- cEIT ------------------------------------------------------------------ class cEIT : public SI::EIT { public: cEIT(cEitTablesHash &EitTablesHash, int Source, u_char Tid, const u_char *Data); }; cEIT::cEIT(cEitTablesHash &EitTablesHash, int Source, u_char Tid, const u_char *Data) :SI::EIT(Data, false) { if (!CheckCRCAndParse()) return; int HashId = getServiceId(); cEitTables *EitTables = EitTablesHash.Get(HashId); if (!EitTables) { EitTables = new cEitTables; EitTablesHash.Add(EitTables, HashId); } bool Process = EitTables->Check(Tid, getVersionNumber(), getSectionNumber()); if (Tid != 0x4E && !Process) // we need to set the 'seen' tag to watch the running status of the present/following event return; time_t Now = time(NULL); if (Now < VALID_TIME) return; // we need the current time for handling PDC descriptors cStateKey ChannelsStateKey; cChannels *Channels = cChannels::GetChannelsWrite(ChannelsStateKey, 10); if (!Channels) return; tChannelID channelID(Source, getOriginalNetworkId(), getTransportStreamId(), getServiceId()); cChannel *Channel = Channels->GetByChannelID(channelID, true); if (!Channel || EpgHandlers.IgnoreChannel(Channel)) { ChannelsStateKey.Remove(false); return; } cStateKey SchedulesStateKey; cSchedules *Schedules = cSchedules::GetSchedulesWrite(SchedulesStateKey, 10); if (!Schedules) { ChannelsStateKey.Remove(false); return; } cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(Channel, true); if (pSchedule->OnActualTp(Tid) && (Tid & 0xF0) == 0x60) { SchedulesStateKey.Remove(false); ChannelsStateKey.Remove(false); return; } if (!EpgHandlers.BeginSegmentTransfer(Channel)) { SchedulesStateKey.Remove(false); ChannelsStateKey.Remove(false); return; } bool ChannelsModified = false; bool handledExternally = EpgHandlers.HandledExternally(Channel); bool Empty = true; bool Modified = false; time_t LingerLimit = Now - EPG_LINGER_TIME; time_t SegmentStart = 0; // these are actually "section" start/end times time_t SegmentEnd = 0; struct tm t = { 0 }; localtime_r(&Now, &t); // this initializes the time zone in 't' SI::EIT::Event SiEitEvent; for (SI::Loop::Iterator it; eventLoop.getNext(SiEitEvent, it); ) { if (EpgHandlers.HandleEitEvent(pSchedule, &SiEitEvent, Tid, getVersionNumber())) continue; // an EPG handler has done all of the processing time_t StartTime = SiEitEvent.getStartTime(); int Duration = SiEitEvent.getDuration(); // Drop bogus events - but keep NVOD reference events, where all bits of the start time field are set to 1, resulting in a negative number. if (StartTime == 0 || StartTime > 0 && Duration == 0) continue; Empty = false; // Ignore events that ended before the "EPG linger time": if (StartTime + Duration < LingerLimit) continue; if (!SegmentStart) SegmentStart = StartTime; SegmentEnd = StartTime + Duration; if (Tid == 0x4E) { if (getSectionNumber() == 0) EitTables->SetTableStart(SegmentStart); else EitTables->SetTableEnd(SegmentEnd); } cEvent *newEvent = NULL; cEvent *rEvent = NULL; cEvent *pEvent = NULL; if (Tid == 0x4E || (Tid & 0xF0) == 0x50) pEvent = const_cast(pSchedule->GetEventById(SiEitEvent.getEventId())); else pEvent = const_cast(pSchedule->GetEventByTime(StartTime)); if (!pEvent || handledExternally) { if (handledExternally && !EpgHandlers.IsUpdate(SiEitEvent.getEventId(), StartTime, Tid, getVersionNumber())) continue; // If we don't have that event yet, we create a new one. // Otherwise we copy the information into the existing event anyway, because the data might have changed. pEvent = newEvent = new cEvent(SiEitEvent.getEventId()); newEvent->SetStartTime(StartTime); newEvent->SetDuration(Duration); if (!handledExternally) pSchedule->AddEvent(newEvent); } else { // We have found an existing event, either through its event ID or its start time. pEvent->SetSeen(); uchar TableID = max(pEvent->TableID(), uchar(0x4E)); // for backwards compatibility, table ids less than 0x4E are treated as if they were "present" // We never overwrite present/following with events from other tables: if (TableID == 0x4E && Tid != 0x4E) continue; if (pEvent->HasTimer()) { if (pEvent->StartTime() != StartTime || pEvent->Duration() != Duration) dsyslog("channel %d (%s) event %s times changed to %s-%s", Channel->Number(), Channel->Name(), *pEvent->ToDescr(), *TimeString(StartTime), *TimeString(StartTime + Duration)); } EpgHandlers.SetEventID(pEvent, SiEitEvent.getEventId()); // unfortunately some stations use different event ids for the same event in different tables :-( EpgHandlers.SetStartTime(pEvent, StartTime); EpgHandlers.SetDuration(pEvent, Duration); } if (pEvent->TableID() > 0x4E) // for backwards compatibility, table ids less than 0x4E are never overwritten pEvent->SetTableID(Tid); if (Tid == 0x4E) { // we trust only the present/following info on the actual TS int RunningStatus = SiEitEvent.getRunningStatus(); #if DBGEIT if (Process) dsyslog("channel %d (%s) event %s status %d (raw data from '%s' section)", Channel->Number(), Channel->Name(), *pEvent->ToDescr(), RunningStatus, getSectionNumber() ? "following" : "present"); #endif if (RunningStatus >= SI::RunningStatusNotRunning) { // Workaround for broadcasters who set an event to status "not running" where // this is inappropriate: if (RunningStatus != pEvent->RunningStatus()) { // if the running status of the event has changed... if (RunningStatus == SI::RunningStatusNotRunning) { // ...and the new status is "not running"... int OverrideStatus = -1; if (getSectionNumber() == 0) { // ...and if this the "present" event... if (pEvent->RunningStatus() == SI::RunningStatusPausing) // ...and if the event has already been set to "pausing"... OverrideStatus = SI::RunningStatusPausing; // ...then we ignore the faulty new status and stay with "pausing" } else // ...and if this is the "following" event... OverrideStatus = SI::RunningStatusUndefined; // ...then we ignore the faulty new status and fall back to "undefined" if (OverrideStatus >= 0) { #if DBGEIT if (Process) dsyslog("channel %d (%s) event %s status %d (ignored status %d from '%s' section)", Channel->Number(), Channel->Name(), *pEvent->ToDescr(), OverrideStatus, RunningStatus, getSectionNumber() ? "following" : "present"); #endif RunningStatus = OverrideStatus; } } } pSchedule->SetRunningStatus(pEvent, RunningStatus, Channel); } if (!Process) continue; } pEvent->SetVersion(getVersionNumber()); int LanguagePreferenceShort = -1; int LanguagePreferenceExt = -1; bool UseExtendedEventDescriptor = false; SI::Descriptor *d; SI::ExtendedEventDescriptors *ExtendedEventDescriptors = NULL; SI::ShortEventDescriptor *ShortEventDescriptor = NULL; cLinkChannels *LinkChannels = NULL; cComponents *Components = NULL; for (SI::Loop::Iterator it2; (d = SiEitEvent.eventDescriptors.getNext(it2)); ) { switch (d->getDescriptorTag()) { case SI::ExtendedEventDescriptorTag: { SI::ExtendedEventDescriptor *eed = (SI::ExtendedEventDescriptor *)d; if (I18nIsPreferredLanguage(Setup.EPGLanguages, eed->languageCode, LanguagePreferenceExt) || !ExtendedEventDescriptors) { delete ExtendedEventDescriptors; ExtendedEventDescriptors = new SI::ExtendedEventDescriptors; UseExtendedEventDescriptor = true; } if (UseExtendedEventDescriptor) { if (ExtendedEventDescriptors->Add(eed)) d = NULL; // so that it is not deleted } if (eed->getDescriptorNumber() == eed->getLastDescriptorNumber()) UseExtendedEventDescriptor = false; } break; case SI::ShortEventDescriptorTag: { SI::ShortEventDescriptor *sed = (SI::ShortEventDescriptor *)d; if (I18nIsPreferredLanguage(Setup.EPGLanguages, sed->languageCode, LanguagePreferenceShort) || !ShortEventDescriptor) { delete ShortEventDescriptor; ShortEventDescriptor = sed; d = NULL; // so that it is not deleted } } break; case SI::ContentDescriptorTag: { SI::ContentDescriptor *cd = (SI::ContentDescriptor *)d; SI::ContentDescriptor::Nibble Nibble; int NumContents = 0; uchar Contents[MaxEventContents] = { 0 }; for (SI::Loop::Iterator it3; cd->nibbleLoop.getNext(Nibble, it3); ) { if (NumContents < MaxEventContents) { Contents[NumContents] = ((Nibble.getContentNibbleLevel1() & 0xF) << 4) | (Nibble.getContentNibbleLevel2() & 0xF); NumContents++; } } EpgHandlers.SetContents(pEvent, Contents); } break; case SI::ParentalRatingDescriptorTag: { int LanguagePreferenceRating = -1; SI::ParentalRatingDescriptor *prd = (SI::ParentalRatingDescriptor *)d; SI::ParentalRatingDescriptor::Rating Rating; for (SI::Loop::Iterator it3; prd->ratingLoop.getNext(Rating, it3); ) { if (I18nIsPreferredLanguage(Setup.EPGLanguages, Rating.languageCode, LanguagePreferenceRating)) { int ParentalRating = (Rating.getRating() & 0xFF); switch (ParentalRating) { // values defined by the DVB standard (minimum age = rating + 3 years): case 0x01 ... 0x0F: ParentalRating += 3; break; // values defined by broadcaster CSAT (now why didn't they just use 0x07, 0x09 and 0x0D?): case 0x11: ParentalRating = 10; break; case 0x12: ParentalRating = 12; break; case 0x13: ParentalRating = 16; break; default: ParentalRating = 0; } EpgHandlers.SetParentalRating(pEvent, ParentalRating); } } } break; case SI::PDCDescriptorTag: { SI::PDCDescriptor *pd = (SI::PDCDescriptor *)d; t.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting int month = t.tm_mon; t.tm_mon = pd->getMonth() - 1; t.tm_mday = pd->getDay(); t.tm_hour = pd->getHour(); t.tm_min = pd->getMinute(); t.tm_sec = 0; if (month == 11 && t.tm_mon == 0) // current month is dec, but event is in jan t.tm_year++; else if (month == 0 && t.tm_mon == 11) // current month is jan, but event is in dec t.tm_year--; time_t vps = mktime(&t); EpgHandlers.SetVps(pEvent, vps); } break; case SI::TimeShiftedEventDescriptorTag: { SI::TimeShiftedEventDescriptor *tsed = (SI::TimeShiftedEventDescriptor *)d; cSchedule *rSchedule = (cSchedule *)Schedules->GetSchedule(tChannelID(Source, Channel->Nid(), Channel->Tid(), tsed->getReferenceServiceId())); if (!rSchedule) break; rEvent = (cEvent *)rSchedule->GetEventById(tsed->getReferenceEventId()); if (!rEvent) break; EpgHandlers.SetTitle(pEvent, rEvent->Title()); EpgHandlers.SetShortText(pEvent, rEvent->ShortText()); EpgHandlers.SetDescription(pEvent, rEvent->Description()); } break; case SI::LinkageDescriptorTag: { SI::LinkageDescriptor *ld = (SI::LinkageDescriptor *)d; tChannelID linkID(Source, ld->getOriginalNetworkId(), ld->getTransportStreamId(), ld->getServiceId()); if (ld->getLinkageType() == SI::LinkageTypePremiere) { // Premiere World bool hit = StartTime <= Now && Now < StartTime + Duration; if (hit) { char linkName[ld->privateData.getLength() + 1]; strn0cpy(linkName, (const char *)ld->privateData.getData(), sizeof(linkName)); // TODO is there a standard way to determine the character set of this string? cChannel *link = Channels->GetByChannelID(linkID); if (link != Channel) { // only link to other channels, not the same one if (link) { if (Setup.UpdateChannels == 1 || Setup.UpdateChannels >= 3) ChannelsModified |= link->SetName(linkName, "", ""); } else if (Setup.UpdateChannels >= 4) { cChannel *Transponder = Channel; if (Channel->Tid() != ld->getTransportStreamId()) Transponder = Channels->GetByTransponderID(linkID); link = Channels->NewChannel(Transponder, linkName, "", "", ld->getOriginalNetworkId(), ld->getTransportStreamId(), ld->getServiceId()); ChannelsModified = true; //XXX patFilter->Trigger(); } if (link) { if (!LinkChannels) LinkChannels = new cLinkChannels; LinkChannels->Add(new cLinkChannel(link)); } } else ChannelsModified |= Channel->SetPortalName(linkName); } } } break; case SI::ComponentDescriptorTag: { SI::ComponentDescriptor *cd = (SI::ComponentDescriptor *)d; uchar Stream = cd->getStreamContent(); uchar Ext = cd->getStreamContentExt(); uchar Type = cd->getComponentType(); if ((1 <= Stream && Stream <= 6 && Type != 0) // 1=MPEG2-video, 2=MPEG1-audio, 3=subtitles, 4=AC3-audio, 5=H.264-video, 6=HEAAC-audio || (Stream == 9 && Ext < 2)) { // 0x09=HEVC-video, 0x19=AC-4-audio if (!Components) Components = new cComponents; char buffer[Utf8BufSize(256)]; if (Stream == 9) Stream |= Ext << 4; Components->SetComponent(Components->NumComponents(), Stream, Type, I18nNormalizeLanguageCode(cd->languageCode), cd->description.getText(buffer, sizeof(buffer))); } } break; default: ; } delete d; } if (!rEvent) { if (ShortEventDescriptor) { char buffer[Utf8BufSize(256)]; EpgHandlers.SetTitle(pEvent, ShortEventDescriptor->name.getText(buffer, sizeof(buffer))); EpgHandlers.SetShortText(pEvent, ShortEventDescriptor->text.getText(buffer, sizeof(buffer))); } else { EpgHandlers.SetTitle(pEvent, NULL); EpgHandlers.SetShortText(pEvent, NULL); } if (ExtendedEventDescriptors) { char buffer[Utf8BufSize(ExtendedEventDescriptors->getMaximumTextLength(": ")) + 1]; EpgHandlers.SetDescription(pEvent, ExtendedEventDescriptors->getText(buffer, sizeof(buffer), ": ")); } else EpgHandlers.SetDescription(pEvent, NULL); } delete ExtendedEventDescriptors; delete ShortEventDescriptor; EpgHandlers.SetComponents(pEvent, Components); EpgHandlers.FixEpgBugs(pEvent); if (LinkChannels) ChannelsModified |= Channel->SetLinkChannels(LinkChannels); Modified = true; EpgHandlers.HandleEvent(pEvent); if (handledExternally) delete pEvent; } if (Tid == 0x4E) { if (Empty && getSectionNumber() == 0) // ETR 211: an empty entry in section 0 of table 0x4E means there is currently no event running pSchedule->ClrRunningStatus(Channel); pSchedule->SetPresentSeen(); } if (Process) { bool Complete = EitTables->Processed(Tid, getLastTableId(), getSectionNumber(), getLastSectionNumber(), getSegmentLastSectionNumber()); if (Modified && (Tid >= 0x50 || Complete)) { // we process the 0x5X tables segment by segment, but 0x4E only if we have received ALL its segments (0 and 1, i.e. "present" and "following") if (Tid == 0x4E && getLastSectionNumber() == 1) { SegmentStart = EitTables->TableStart(); SegmentEnd = EitTables->TableEnd(); } EpgHandlers.SortSchedule(pSchedule); EpgHandlers.DropOutdated(pSchedule, SegmentStart, SegmentEnd, Tid, getVersionNumber()); } } EpgHandlers.EndSegmentTransfer(Modified); SchedulesStateKey.Remove(Modified); ChannelsStateKey.Remove(ChannelsModified); } // --- cTDT ------------------------------------------------------------------ #define MAX_TIME_DIFF 1 // number of seconds the local time may differ from dvb time before making any corrections #define MAX_ADJ_DIFF 10 // number of seconds the local time may differ from dvb time to allow smooth adjustment #define ADJ_DELTA 300 // number of seconds between calls for smooth time adjustment class cTDT : public SI::TDT { private: static cMutex mutex; static time_t lastAdj; public: cTDT(const u_char *Data); }; cMutex cTDT::mutex; time_t cTDT::lastAdj = 0; cTDT::cTDT(const u_char *Data) :SI::TDT(Data, false) { CheckParse(); time_t dvbtim = getTime(); time_t loctim = time(NULL); int diff = dvbtim - loctim; if (abs(diff) > MAX_TIME_DIFF) { mutex.Lock(); if (abs(diff) > MAX_ADJ_DIFF) { timespec ts = {}; ts.tv_sec = dvbtim; if (clock_settime(CLOCK_REALTIME, &ts) == 0) isyslog("system time changed from %s (%jd) to %s (%jd)", *TimeToString(loctim), intmax_t(loctim), *TimeToString(dvbtim), intmax_t(dvbtim)); else esyslog("ERROR while setting system time: %m"); } else if (time(NULL) - lastAdj > ADJ_DELTA) { lastAdj = time(NULL); timeval delta; delta.tv_sec = diff; delta.tv_usec = 0; if (adjtime(&delta, NULL) == 0) isyslog("system time adjustment initiated from %s (%jd) to %s (%jd)", *TimeToString(loctim), intmax_t(loctim), *TimeToString(dvbtim), intmax_t(dvbtim)); else esyslog("ERROR while adjusting system time: %m"); } mutex.Unlock(); } } // --- cEitFilter ------------------------------------------------------------ time_t cEitFilter::disableUntil = 0; cEitFilter::cEitFilter(void) { Set(0x12, 0x40, 0xC0); // event info present&following actual/other TS (0x4E/0x4F), future actual/other TS (0x5X/0x6X) Set(0x14, 0x70); // TDT } void cEitFilter::SetStatus(bool On) { cMutexLock MutexLock(&mutex); cFilter::SetStatus(On); eitTablesHash.Clear(); } void cEitFilter::SetDisableUntil(time_t Time) { disableUntil = Time; } void cEitFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length) { cMutexLock MutexLock(&mutex); if (disableUntil) { if (time(NULL) > disableUntil) disableUntil = 0; else return; } switch (Pid) { case 0x12: { if (Tid == 0x4E || Tid >= 0x50 && Tid <= 0x6F) // we ignore 0x4F, which only causes trouble cEIT EIT(eitTablesHash, Source(), Tid, Data); } break; case 0x14: { if (Setup.SetSystemTime && Setup.TimeSource == Source() && Setup.TimeTransponder && ISTRANSPONDER(Transponder(), Setup.TimeTransponder)) cTDT TDT(Data); } break; default: ; } }