From: Oliver Sluke <22557015+oliversluke@users.noreply.github.com> Date: Thu, 21 May 2026 22:04:08 +0000 (+0200) Subject: epggrab: per-service EIT processing policy with global default X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;ds=sidebyside;p=thirdparty%2Ftvheadend.git epggrab: per-service EIT processing policy with global default DVB EIT comes in two forms. Actual transport stream tables (table_id 0x4e present/following, 0x50-0x5f schedule) describe the multiplex that carries them. Other transport stream tables (0x4f, 0x60-0x6f) describe services on a different multiplex. The OTA grabber tunes every multiplex, so when one multiplex carries other-TS EIT for a service whose own multiplex is also grabbed, both descriptions reach the EPG store and the last writer wins. A coarse other-TS placeholder from a neighbouring multiplex can then overwrite a service's own detailed schedule, and vice versa, so the EPG flips between the two on successive grabs. Replace the per-service "Ignore EPG (EIT)" boolean with an "EIT processing" enum, and add a matching "EIT processing (default)" global setting. Per-service values: Default Defer to the global default None Ignore EIT for this service Actual only Accept only actual-TS sub-tables (0x4e, 0x50-0x5f) Other only Accept only other-TS sub-tables (0x4f, 0x60-0x6f) Either Accept both (today's behaviour, no precedence) Adaptive Accept both, but once the service's own actual-TS schedule has been seen, drop further other-TS for it so a neighbour's coarse description cannot overwrite the detailed schedule The global default exposes the same enum minus Default. New installs get Either, which matches today's behaviour with no surprise migrations. The legacy "dvb_ignore_eit" key is read on first load: true migrates to None (same observable behaviour), false to Default (defers to the global, also same observable behaviour while global stays at Either). The old key disappears from the config on next save. The EIT handler resolves the effective policy per call (per-service unless Default, in which case the global applies) and filters tableid acceptance accordingly. Adaptive tracks per-service "actual-TS schedule seen" at runtime; the flag is not persisted, so it is relearned each session. P/F (0x4e / 0x4f) is only two events per service and is not used as the seen trigger. Full per-value help text lives in docs/property/eit_processing.md and docs/property/eit_processing_default.md, wired in via the PROP_DOC macro; the in-form ".desc" tooltips are short pointers to the Help icon, matching the existing pattern (e.g. PROP_DOC(cron), PROP_DOC(ota_genre_translation)). Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com> --- diff --git a/docs/property/eit_processing.md b/docs/property/eit_processing.md new file mode 100644 index 000000000..4c2149bc8 --- /dev/null +++ b/docs/property/eit_processing.md @@ -0,0 +1,51 @@ + + +This setting controls which Event Information Table (EIT) sub-tables +are accepted when building the EPG for this service. + +DVB EIT is broadcast in two forms, distinguished by `table_id`: + +- **Actual transport stream EIT** (`0x4e` / `0x50–0x5f`) — broadcast + by the service's *own* multiplex. Describes the service itself. +- **Other transport stream EIT** (`0x4f` / `0x60–0x6f`) — broadcast + by *another* multiplex describing this one. Often coarser; mostly + used so a receiver tuned to one multiplex can still show some EPG + for services on neighbouring multiplexes. + +Without a precedence rule, the two sources can overwrite each other +on every grab cycle, making the EPG flip between a detailed +schedule (from actual-TS) and a coarser placeholder (from other-TS). + +| Value | Behaviour | +| --- | --- | +| **Default** | Defer to the global *EIT processing (default)* setting under EPG Grabber → OTA. Use this for most services. | +| **None** | Ignore all EIT for this service. Equivalent to the legacy *Ignore EPG (EIT)* toggle. | +| **Actual transport stream only** | Accept only actual-TS sub-tables (`0x4e`, `0x50–0x5f`). | +| **Other transport stream only** | Accept only other-TS sub-tables (`0x4f`, `0x60–0x6f`). | +| **Either** | Accept both, no precedence. Today's default behaviour — what tvheadend has always done. | +| **Adaptive** | Accept both, but once this service's own actual-TS schedule has been seen this session, drop further other-TS for it. A neighbour's coarse description cannot overwrite the service's detailed schedule, while services whose actual-TS is never received keep using other-TS (so a dedicated EPG multiplex still works). | + +The Adaptive policy is self-correcting per service: it suppresses +other-TS only once detailed actual-TS data has actually been +received. Inspired by VDR's adaptive EIT handling. + +**One operational caveat for Adaptive.** The "actual-TS observed" +state is per session and resets when tvheadend restarts. For a +short window at startup, other-TS may briefly overwrite the +detailed EPG until the service's own actual-TS schedule is re- +observed in the new session. DVR entries whose link to an EPG +event was made before the restart are reconnected by the existing +fuzzy-match fallback in `dvr_entry_fuzzy_match` once actual-TS +data returns. The one remaining edge is a recording already in +progress at restart that happens to receive a stop-time-extension +update during this swap window — the extension lands on the +placeholder rather than the resumed entry, so the recording stops +at its originally-scheduled time. Across normal operation (no +restart), Adaptive self-heals after the first actual-TS arrival +and the placeholder is then permanently filtered for that service. + +Most installations should leave this at *Default* and pick the +policy globally. Per-service overrides are useful for diagnosing or +working around broadcaster-specific quirks — a service whose own +actual-TS EIT is intermittent, or one where you trust the +neighbour's other-TS more than the service's own. diff --git a/docs/property/eit_processing_default.md b/docs/property/eit_processing_default.md new file mode 100644 index 000000000..288abbbeb --- /dev/null +++ b/docs/property/eit_processing_default.md @@ -0,0 +1,46 @@ + + +This setting is the default EIT (Event Information Table) +processing policy applied to every DVB service whose own *EIT +processing* is left at *Default*. + +DVB EIT is broadcast in two forms, distinguished by `table_id`: + +- **Actual transport stream EIT** (`0x4e` / `0x50–0x5f`) — broadcast + by the service's *own* multiplex. Describes the service itself. +- **Other transport stream EIT** (`0x4f` / `0x60–0x6f`) — broadcast + by *another* multiplex describing this one. Often coarser. + +Without a precedence rule, the two sources can overwrite each other +on every grab cycle, making the EPG flip between a detailed +schedule (from actual-TS) and a coarser placeholder (from other-TS). + +| Value | Behaviour | +| --- | --- | +| **None** | Ignore all EIT for services set to Default. | +| **Actual transport stream only** | Accept only actual-TS sub-tables (`0x4e`, `0x50–0x5f`). | +| **Other transport stream only** | Accept only other-TS sub-tables (`0x4f`, `0x60–0x6f`). | +| **Either** | Accept both, no precedence. Today's default behaviour — what tvheadend has always done. | +| **Adaptive** | Accept both, but once a service's own actual-TS schedule has been seen this session, drop further other-TS for it. A neighbouring multiplex's coarse description cannot overwrite the service's detailed schedule. Services whose actual-TS is never received keep using other-TS, so a dedicated EPG multiplex still works. | + +Fresh installations default to **Either**, which preserves the +historical behaviour. Switch to **Adaptive** to opt into the +actual-TS-precedence heuristic without configuring services +individually. + +**One operational caveat for Adaptive.** The "actual-TS observed" +state is per session and resets when tvheadend restarts. For a +short window at startup, other-TS may briefly overwrite the +detailed EPG until each service's own actual-TS schedule is re- +observed in the new session. DVR entries whose link to an EPG +event was made before the restart are reconnected by the existing +fuzzy-match fallback in `dvr_entry_fuzzy_match` once actual-TS +data returns. The one remaining edge is a recording already in +progress at restart that happens to receive a stop-time-extension +update during this swap window — the extension lands on the +placeholder rather than the resumed entry, so the recording stops +at its originally-scheduled time. Across normal operation (no +restart), Adaptive self-heals after the first actual-TS arrival. + +Any service can override this global default via its own *EIT +processing* setting. diff --git a/src/epggrab.c b/src/epggrab.c index 2ce352f04..a8aa165c3 100644 --- a/src/epggrab.c +++ b/src/epggrab.c @@ -333,9 +333,25 @@ epggrab_class_ota_genre_translation_notify(void *self, const char *lang) epggrab_ota_set_genre_translation(); } +static htsmsg_t * +epggrab_class_eit_processing_default_list(void *o, const char *lang) +{ + /* Per-service EIT_PROCESSING_DEFAULT defers here, so the global + * choice list omits Default itself. */ + static const struct strtab tab[] = { + { N_("None"), EIT_PROCESSING_NONE }, + { N_("Actual transport stream only"), EIT_PROCESSING_ACTUAL_ONLY }, + { N_("Other transport stream only"), EIT_PROCESSING_OTHER_ONLY }, + { N_("Either"), EIT_PROCESSING_EITHER }, + { N_("Adaptive"), EIT_PROCESSING_ADAPTIVE }, + }; + return strtab2htsmsg(tab, 1, lang); +} + CLASS_DOC(epgconf) PROP_DOC(cron) PROP_DOC(ota_genre_translation) +PROP_DOC(eit_processing_default) const idclass_t epggrab_class = { .ic_snode = &epggrab_conf.idnode, @@ -466,6 +482,20 @@ const idclass_t epggrab_class = { .opts = PO_ADVANCED, .group = 3, }, + { + .type = PT_INT, + .id = "eit_processing_default", + .name = N_("EIT processing (default)"), + .desc = N_("Default EIT processing policy applied to " + "services whose own \"EIT processing\" is set " + "to Default. See Help for the full policy " + "descriptions."), + .doc = prop_doc_eit_processing_default, + .off = offsetof(epggrab_conf_t, eit_processing_default), + .opts = PO_ADVANCED | PO_DOC_NLIST, + .list = epggrab_class_eit_processing_default_list, + .group = 3, + }, { .type = PT_STR, .id = "ota_cron", @@ -587,6 +617,7 @@ void epggrab_init ( void ) epggrab_conf.epgdb_periodicsave = 0; epggrab_conf.epgdb_saveafterimport = 0; epggrab_conf.epgdb_processparentallabels = 0; + epggrab_conf.eit_processing_default = EIT_PROCESSING_EITHER; epggrab_cron_multi = NULL; diff --git a/src/epggrab.h b/src/epggrab.h index 974bfd615..8266eff38 100644 --- a/src/epggrab.h +++ b/src/epggrab.h @@ -317,6 +317,22 @@ struct epggrab_module_ota_scraper /* * */ +/* + * EIT processing policy. Per-service value selects which EIT sub-tables + * are accepted for that service; the per-service Default value defers + * to the global default (which therefore omits Default from its choice + * list). Adaptive accepts both actual- and other-TS but, once the + * service's own actual-TS schedule has been received, drops further + * other-TS for it so a neighbouring multiplex's coarse description + * cannot overwrite the service's detailed schedule. + */ +#define EIT_PROCESSING_DEFAULT 0 /* per-service only: use global */ +#define EIT_PROCESSING_NONE 1 +#define EIT_PROCESSING_ACTUAL_ONLY 2 +#define EIT_PROCESSING_OTHER_ONLY 3 +#define EIT_PROCESSING_EITHER 4 +#define EIT_PROCESSING_ADAPTIVE 5 + typedef struct epggrab_conf { idnode_t idnode; char *cron; @@ -330,6 +346,7 @@ typedef struct epggrab_conf { char *ota_genre_translation; uint32_t ota_timeout; uint32_t ota_initial; + uint32_t eit_processing_default; uint32_t int_initial; } epggrab_conf_t; diff --git a/src/epggrab/module/eit.c b/src/epggrab/module/eit.c index e6213bee1..447acca5c 100644 --- a/src/epggrab/module/eit.c +++ b/src/epggrab/module/eit.c @@ -1285,8 +1285,54 @@ svc_ok: if (!LIST_FIRST(&svc->s_channels)) goto done; - if (svc->s_dvb_ignore_eit) + /* Apply the effective EIT processing policy for this service: + * the per-service value, falling back to the global default when + * the service is set to Default. Tableid ranges in this callback + * (see the bounds check earlier in this function) are + * 0x4e actual-TS present/following + * 0x50-0x5f actual-TS schedule + * 0x4f, 0x60-0x6f other-TS present/following + schedule. */ + int processing = svc->s_dvb_eit_processing; + if (processing == EIT_PROCESSING_DEFAULT) + processing = epggrab_conf.eit_processing_default; + + switch (processing) { + case EIT_PROCESSING_NONE: goto done; + case EIT_PROCESSING_ACTUAL_ONLY: + if (tableid == 0x4f || tableid >= 0x60) + goto done; + break; + case EIT_PROCESSING_OTHER_ONLY: + if (tableid == 0x4e || (tableid >= 0x50 && tableid < 0x60)) + goto done; + break; + case EIT_PROCESSING_ADAPTIVE: + /* Track actual-TS schedule arrival per service. Once seen, + * drop further other-TS for it so a neighbouring multiplex's + * coarse description cannot overwrite the service's own + * detailed schedule. Services whose actual-TS is never + * received keep using other-TS, so a dedicated EPG multiplex + * still works. P/F (0x4e / 0x4f) is only two events per + * service and is not used as the "actual-TS seen" trigger. */ + if (tableid >= 0x50 && tableid < 0x60) { + if (!svc->s_dvb_eit_actual_seen) { + svc->s_dvb_eit_actual_seen = 1; + tvhtrace(LS_TBL_EIT, "%s: %s actual-TS schedule seen", + mt->mt_name, svc->s_nicename); + } + } else if ((tableid == 0x4f || tableid >= 0x60) && + svc->s_dvb_eit_actual_seen) { + tvhtrace(LS_TBL_EIT, + "%s: skip other-TS tid 0x%02X for %s, actual-TS seen", + mt->mt_name, tableid, svc->s_nicename); + goto done; + } + break; + case EIT_PROCESSING_EITHER: + default: + break; + } /* Queue events */ len -= 11; diff --git a/src/input/mpegts.h b/src/input/mpegts.h index 0fb5b1922..6aab3fdc1 100644 --- a/src/input/mpegts.h +++ b/src/input/mpegts.h @@ -578,7 +578,7 @@ struct mpegts_service char *s_dvb_provider; char *s_dvb_cridauth; uint16_t s_dvb_servicetype; - int s_dvb_ignore_eit; + int s_dvb_eit_processing; //EIT processing policy (EIT_PROCESSING_* in epggrab.h); Default defers to the global setting int s_dvb_subtitle_processing; //Various options for replacing/augmenting the desc from the sub-title int s_dvb_ignore_matching_subtitle; //Ignore the sub-title if same as title char *s_dvb_charset; @@ -593,6 +593,11 @@ struct mpegts_service */ int s_dvb_eit_enable; + /* Runtime flag: this service's own actual-TS schedule EIT + * (table_id 0x50-0x5f) has been received this session. Used by + * the EIT_PROCESSING_ADAPTIVE policy to start dropping other-TS + * once detailed actual-TS data is available. Not persisted. */ + int s_dvb_eit_actual_seen; uint64_t s_dvb_opentv_chnum; uint16_t s_dvb_opentv_id; uint16_t s_atsc_source_id; diff --git a/src/input/mpegts/mpegts_service.c b/src/input/mpegts/mpegts_service.c index 12cae7283..034fed77f 100644 --- a/src/input/mpegts/mpegts_service.c +++ b/src/input/mpegts/mpegts_service.c @@ -94,7 +94,42 @@ mpegts_service_subtitle_procesing ( void *o, const char *lang ) return strtab2htsmsg(tab, 1, lang); } +static htsmsg_t * +mpegts_service_eit_processing_list ( void *o, const char *lang ) +{ + static const struct strtab tab[] = { + { N_("Default (use global setting)"), EIT_PROCESSING_DEFAULT }, + { N_("None"), EIT_PROCESSING_NONE }, //Ignore EIT for this service. + { N_("Actual transport stream only"), EIT_PROCESSING_ACTUAL_ONLY }, + { N_("Other transport stream only"), EIT_PROCESSING_OTHER_ONLY }, + { N_("Either"), EIT_PROCESSING_EITHER }, + { N_("Adaptive"), EIT_PROCESSING_ADAPTIVE }, //Drop other-TS once actual-TS schedule has been seen. + }; + return strtab2htsmsg(tab, 1, lang); +} + +/* Translate the legacy dvb_ignore_eit bool into dvb_eit_processing on + * first load of an existing config. true → None (ignore EIT for this + * service), false → Default (defer to the global setting). Only run + * when the new key is absent, so already-migrated configs are + * untouched; the old key naturally disappears on next save (no + * matching prop on the class). */ +static void +mpegts_service_class_load(struct idnode *self, htsmsg_t *c) +{ + mpegts_service_t *s = (mpegts_service_t *)self; + int b; + + service_load((service_t *)self, c); + + if (!htsmsg_field_find(c, "dvb_eit_processing") && + !htsmsg_get_bool(c, "dvb_ignore_eit", &b)) + s->s_dvb_eit_processing = b ? EIT_PROCESSING_NONE + : EIT_PROCESSING_DEFAULT; +} + CLASS_DOC(mpegts_service) +PROP_DOC(eit_processing) const idclass_t mpegts_service_class = { @@ -103,6 +138,7 @@ const idclass_t mpegts_service_class = .ic_caption = N_("DVB Inputs - Services"), .ic_doc = tvh_doc_mpegts_service_class, .ic_order = "enabled,channel,svcname", + .ic_load = mpegts_service_class_load, .ic_properties = (const property_t[]){ { .type = PT_STR, @@ -203,13 +239,16 @@ const idclass_t mpegts_service_class = .off = offsetof(mpegts_service_t, s_dvb_servicetype), }, { - .type = PT_BOOL, - .id = "dvb_ignore_eit", - .name = N_("Ignore EPG (EIT)"), - .desc = N_("Enable or disable ignoring of Event Information " - "Table (EIT) data for this service."), - .off = offsetof(mpegts_service_t, s_dvb_ignore_eit), - .opts = PO_EXPERT, + .type = PT_INT, + .id = "dvb_eit_processing", + .name = N_("EIT processing"), + .desc = N_("Which EIT sub-tables are accepted for this " + "service. Default defers to the global setting. " + "See Help for the full policy descriptions."), + .doc = prop_doc_eit_processing, + .off = offsetof(mpegts_service_t, s_dvb_eit_processing), + .opts = PO_EXPERT | PO_DOC_NLIST, + .list = mpegts_service_eit_processing_list, }, { .type = PT_INT,