]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
epggrab: per-service EIT processing policy with global default master
authorOliver Sluke <22557015+oliversluke@users.noreply.github.com>
Thu, 21 May 2026 22:04:08 +0000 (00:04 +0200)
committerFlole <Flole998@users.noreply.github.com>
Sun, 7 Jun 2026 02:02:03 +0000 (04:02 +0200)
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>
docs/property/eit_processing.md [new file with mode: 0644]
docs/property/eit_processing_default.md [new file with mode: 0644]
src/epggrab.c
src/epggrab.h
src/epggrab/module/eit.c
src/input/mpegts.h
src/input/mpegts/mpegts_service.c

diff --git a/docs/property/eit_processing.md b/docs/property/eit_processing.md
new file mode 100644 (file)
index 0000000..4c2149b
--- /dev/null
@@ -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 (file)
index 0000000..288abbb
--- /dev/null
@@ -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.
index 2ce352f0495c8066f9b9279f03b913f6cb367fd8..a8aa165c34af4a05cddfa80ccabf0fe3649d7f68 100644 (file)
@@ -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;
 
index 974bfd6151f60bafddf0dd17473e19a611816b97..8266eff385684c3444345b20b150d05dd019780c 100644 (file)
@@ -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;
 
index e6213bee1bd30a2bc2cddee73ad7354f76b64854..447acca5c0ed58dfbf05a5aedda2eda115431e8b 100644 (file)
@@ -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;
index 0fb5b192279f0dceb963b26f9a21d91ba3e1a915..6aab3fdc1ffbdaab966b0eebc1280f3b5852f849 100644 (file)
@@ -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;
index 12cae7283809c3056c78ac80f8b66d6c3ccfad6b..034fed77f4bba170d084a11aef5b543d8632cf0f 100644 (file)
@@ -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,