]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
dvr: Prefer earlier/better schedule events for autorecs.
authorE.Smith <31170571+azlm8t@users.noreply.github.com>
Fri, 14 Sep 2018 20:48:27 +0000 (21:48 +0100)
committerJaroslav Kysela <perex@perex.cz>
Thu, 20 Sep 2018 12:28:36 +0000 (14:28 +0200)
We now try to avoid scheduling on a timeshift channel X+1 if we have
the same programme on channel X. If a programme is on at the same time
on multiple channels then we will try and schedule on a better
channel, such as one with more services (such as mixed DVB-T/DVB-S) to
allow service switching fallback on tuner conflict.

An example of the new log entry:

dvr: Autorecord "movie misc" Replacing existing dvr recording entry of
"The Departed" on ITV4+1 @ start 2018-09-20;00:35:00(+0100) with
recording on ITV4 @ start 2018-09-19;23:35:00(+0100)

Context for the change is below:

In some areas, a broadcast programme might be shown at the same time
on multiple channels, or at different times on various timeshift,
repeat, or regional channels.

An example of this is the Astra 28.2E satellite where BBC1 has
multiple (26) regional channels that (mostly) show the same
programmes, but sometimes a region might have a +30m or +1h timeshift
for a single programme.

Similarly the commercial channel "Channel 4" has "Channel 4+1", and a
repeat channel of "4Seven" and various associated HD channels. Homes
receive all these regional channels via one dish and are required for
BBC to manually switch between HD/SD versions for local interest
programmes such as regional news.

Previously, when setting an autorec, the item chosen to record was
based on the first broadcast that matched the criteria (such as film
title). However, this broadcast is not necessarily the earliest or the
best.

So this meant that with timeshift channels, a programme could be
scheduled on X+1 instead of X, so record at 21:00 instead of 20:00.

On other setups, the event might correctly record at 20:00, since
scheduling depended on internal structures and which broadcasts are
found first (such as via OTA updates).

Similarly, in countries where the same programme can be received but
on different channels in different qualities, it was possible to
schedule on a non-HD channel, even though the user wanted HD purely
because the non-HD broadcast was found first and the channels were not
merged.

So we now check our recording list to determine if the event is better
than the already scheduled event. If so, the existing recording event
is replaced.

For our criteria, "better" is defined in the function
dvr_is_better_recording_timeslot, and has a variety of criteria such
as "matches service filter", "earlier start", and "has more services"
(so more likely to be able to failover if there is a problem).  If
other criteria are equal, then we use the channel with the lowest
channel number since Europe EPG has lower channel numbers for
'better' channels.

When determining if an autorec can be scheduled in a better timeslot,
we must only check against other autorecs and not a manually scheduled
entry since the user might schedule a recording at a later date
specifically to avoid conflicts.

This is achieved by replacing the old scheduled recording with a new
scheduled recording. A new log message indicates when this occurs.

Performance: Initial testing suggests that this rescheduling can occur
between zero and two times per programme (when there is an initial
schedule on +1, then a reschedule on non-timeshift SD, then final
schedule on HD). However, it should not add significant runtime
overhead for most people.

src/channels.c
src/channels.h
src/dvr/dvr_db.c
src/service.c
src/service.h

index 7e0c24bcc3c9b89b729e62fdb0364c1391c4dc68..47efe783dccd2ddbe2825f55850e12e08b9d78ff 100644 (file)
@@ -894,7 +894,7 @@ channel_rename_and_save ( const char *from, const char *to )
 }
 
 int64_t
-channel_get_number ( channel_t *ch )
+channel_get_number ( const channel_t *ch )
 {
   int64_t n = 0;
   idnode_list_mapping_t *ilm;
@@ -1969,3 +1969,22 @@ channel_tag_done ( void )
     channel_tag_destroy(ct, 0);
   pthread_mutex_unlock(&global_lock);
 }
+
+int
+channel_has_correct_service_filter(const channel_t *ch, int svf)
+{
+  const idnode_list_mapping_t *ilm;
+  const service_t *service;
+  if (!ch || !svf || svf == PROFILE_SVF_NONE)
+    return 1;
+
+  LIST_FOREACH(ilm, &ch->ch_services, ilm_in2_link) {
+    service = (const service_t*)ilm->ilm_in1;
+    if ((svf == PROFILE_SVF_SD && service_is_sdtv(service)) ||
+        (svf == PROFILE_SVF_HD && service_is_hdtv(service)) ||
+        (svf == PROFILE_SVF_UHD && service_is_uhdtv(service))) {
+      return 1;
+    }
+  }
+  return 0;
+}
index 6e4d72ee4e42c43f188f57eca274d383ed3b8214..5d4bd79fb5e10dd9c4380935321be0d7927f4f22 100644 (file)
@@ -197,7 +197,7 @@ char *channel_get_ename ( channel_t *ch, char *dst, size_t dstlen,
 static inline uint32_t channel_get_major ( int64_t chnum ) { return chnum / CHANNEL_SPLIT; }
 static inline uint32_t channel_get_minor ( int64_t chnum ) { return chnum % CHANNEL_SPLIT; }
 
-int64_t channel_get_number ( channel_t *ch );
+int64_t channel_get_number ( const channel_t *ch );
 int channel_set_number ( channel_t *ch, uint32_t major, uint32_t minor );
 
 char *channel_get_number_as_str ( channel_t *ch, char *dst, size_t dstlen );
@@ -220,5 +220,6 @@ channel_t **channel_get_sorted_list_for_tag
   ( const char *sort_type, channel_tag_t *tag, int *_count );
 channel_tag_t **channel_tag_get_sorted_list
   ( const char *sort_type, int *_count );
+int channel_has_correct_service_filter(const channel_t *ch, int svf);
 
 #endif /* CHANNELS_H */
index e1a087128a9f780ad1b6f39a56401e327ad72744..8ceed85632f178dfe0127a16ba61525ac7cdac21 100644 (file)
@@ -1698,6 +1698,99 @@ static dvr_entry_t *_dvr_duplicate_event(dvr_entry_t *de)
   return NULL;
 }
 
+/*** Return non-zero if the broadcast new_bcast is better for recording than the one for old_de. */
+static int
+dvr_is_better_recording_timeslot(const epg_broadcast_t *new_bcast, const dvr_entry_t *old_de)
+{
+  if (!old_de || !old_de->de_bcast) return 1;            /* Old broadcast should always exist */
+  int old_services = 0;
+  int new_services = 0;
+  int64_t old_chnumber, new_chnumber;
+  const idnode_list_mapping_t *ilm;
+  const epg_broadcast_t *old_bcast = old_de->de_bcast;
+  const channel_t *old_channel = old_bcast->channel;
+  const channel_t *new_channel = new_bcast->channel;
+
+  /* Sanity check. */
+  if (!new_channel) return 0;
+  if (!old_channel) return 1;
+
+  /* Always prefer a recording that has the correct service profile
+   * (UHD, HD, SD).  Someone mentioned (#1846) that some channels can
+   * show a recording earlier in the week in SD then later in the week
+   * in HD so this would prefer the later HD recording if the user so
+   * desired.
+   */
+  if (old_de->de_config && old_de->de_config->dvr_profile &&
+      old_de->de_config->dvr_profile->pro_svfilter != PROFILE_SVF_NONE) {
+    const int svf = old_de->de_config->dvr_profile->pro_svfilter;
+    int old_has_svf = channel_has_correct_service_filter(old_channel, svf);
+    int new_has_svf = channel_has_correct_service_filter(new_channel, svf);
+
+    if (old_has_svf && !new_has_svf)
+      return 0;
+    if (!old_has_svf && new_has_svf)
+      return 1;
+    /* Also try "downgrading", where user asks for UHD, which we don't
+     * have, but we could give them HD.
+     */
+    if (svf == PROFILE_SVF_UHD && !old_has_svf) {
+      old_has_svf = channel_has_correct_service_filter(old_channel, PROFILE_SVF_HD);
+      new_has_svf = channel_has_correct_service_filter(new_channel, PROFILE_SVF_HD);
+
+      if (old_has_svf && !new_has_svf)
+        return 0;
+      if (!old_has_svf && new_has_svf)
+        return 1;
+    }
+  }
+
+  /* Ealier start time is better; prefers non-timeshift channel X to X+1.
+   * This gives us time to reschedule to X+1 if the recording on X fails.
+   */
+  if (new_bcast->start < old_bcast->start)
+    return 1;
+
+  /* Later broadcast is always worse. */
+  if (new_bcast->start > old_bcast->start)
+    return 0;
+
+  /* If here, we have the same time. */
+
+  /* Count the number of services each has. So, if a channel has multiple
+   * services we assume it's a better choice since we have more fallbacks
+   * if a tune fails.
+   */
+  LIST_FOREACH(ilm, &old_channel->ch_services, ilm_in2_link) {
+    ++old_services;
+  }
+  LIST_FOREACH(ilm, &new_channel->ch_services, ilm_in2_link) {
+    ++new_services;
+  }
+  if (new_services > old_services)
+    return 1;
+  if (old_services > new_services)
+    return 0;
+
+  /* Assume lower channel number is a better channel since they
+   * typically paid more to be higher up the EPG.
+   */
+  old_chnumber = channel_get_number(old_channel);
+  new_chnumber = channel_get_number(new_channel);
+  /* Prefer channels with a number to ones without a number */
+  if (!new_chnumber && old_chnumber)
+    return 0;
+  if (new_chnumber && !old_chnumber)
+    return 1;
+  if (new_chnumber < old_chnumber)
+    return 1;
+  if (new_chnumber > old_chnumber)
+    return 0;
+
+  /* All things being equal, prefer the existing recording */
+  return 0;
+}
+
 /**
  *
  */
@@ -1705,8 +1798,9 @@ void
 dvr_entry_create_by_autorec(int enabled, epg_broadcast_t *e, dvr_autorec_entry_t *dae)
 {
   char buf[512];
+  char t1buf[32], t2buf[32];
   const char *s;
-  dvr_entry_t *de;
+  dvr_entry_t *de, *replace = NULL;
   uint32_t count = 0, max_count;
   htsmsg_t *conf;
 
@@ -1714,8 +1808,53 @@ dvr_entry_create_by_autorec(int enabled, epg_broadcast_t *e, dvr_autorec_entry_t
      NOTE: Semantic duplicate detection is deferred to the start time of recording and then done using _dvr_duplicate_event by dvr_timer_start_recording. */
   LIST_FOREACH(de, &dvrentries, de_global_link) {
     if (de->de_bcast == e || epg_episode_match(de->de_bcast, e))
-      if (strcmp(dae->dae_owner ?: "", de->de_owner ?: "") == 0)
-        return;
+      if (strcmp(dae->dae_owner ?: "", de->de_owner ?: "") == 0) {
+        /* See if our new broadcast is better than our existing schedule */
+
+        /* Our autorec can never be better than a manually scheduled programme
+         * since user might schedule to avoid conflicts.
+         */
+        if (!de->de_autorec)
+          return;
+
+        /* Same broadcast, so new one can't be any better. */
+        if (de->de_bcast == e)
+          return;
+
+        /* Existing entry wasn't enabled? If so assume user does not
+         * want a new autorec scheduled that is enabled.
+         */
+        if (!de->de_enabled)
+          return;
+
+        /* If our new broadcast is "better" than the existing
+         * scheduled one, then the existing one can be
+         * replaced. Otherwise, we can return here and use the
+         * existing one as being the best recording to make.
+         */
+        if (!dvr_is_better_recording_timeslot(e, de))
+          return;
+
+        /* New broadcast is better than existing one that is
+         * scheduled. However, we still need to search to end of list
+         * since the new broadcast may already be scheduled somewhere
+         * else in the dvrentries.
+         */
+        replace = de;
+      }
+  }
+
+  /* Have an entry that is worse than our new broadcast so remove it now
+   * so that our max schedules check will be correct.
+   */
+  if (replace) {
+    tvhinfo(LS_DVR, "Autorecord \"%s\" Replacing existing dvr recording entry of \"%s\" on %s @ start %s with recording on %s @ start %s",
+            dae->dae_name, lang_str_get(e->title, NULL),
+            DVR_CH_NAME(replace),
+            gmtime2local(replace->de_bcast->start, t1buf, sizeof t1buf),
+            e->channel ? channel_get_name(e->channel, channel_blank_name) : channel_blank_name,
+            gmtime2local(e->start, t2buf, sizeof t2buf));
+    dvr_entry_destroy(replace, 1);
   }
 
   /* Handle max schedules limit for autorrecord */
index 090c8356b4dadc83c30755def808f35b7e747152..cc37316e4a35026c3ef5b93558aee1a83df5a0ae 100644 (file)
@@ -815,7 +815,7 @@ service_data_timeout(void *aux)
 }
 
 int
-service_is_sdtv(service_t *t)
+service_is_sdtv(const service_t *t)
 {
   char s_type;
   if(t->s_type_user == ST_UNSET)
@@ -834,7 +834,7 @@ service_is_sdtv(service_t *t)
 }
 
 int
-service_is_hdtv(service_t *t)
+service_is_hdtv(const service_t *t)
 {
   char s_type;
   if(t->s_type_user == ST_UNSET)
@@ -854,7 +854,7 @@ service_is_hdtv(service_t *t)
 }
 
 int
-service_is_uhdtv(service_t *t)
+service_is_uhdtv(const service_t *t)
 {
   char s_type;
   if(t->s_type_user == ST_UNSET)
@@ -876,7 +876,7 @@ service_is_uhdtv(service_t *t)
  *
  */
 int
-service_is_radio(service_t *t)
+service_is_radio(const service_t *t)
 {
   int ret = 0;
   char s_type;
@@ -902,7 +902,7 @@ service_is_radio(service_t *t)
  * Is encrypted
  */
 int
-service_is_encrypted(service_t *t)
+service_is_encrypted(const service_t *t)
 {
   elementary_stream_t *st;
   if (((mpegts_service_t *)t)->s_dvb_forcecaid == 0xffff)
index d5f439194f6151ca413ac76da183e126e4379002..ed659ca56882af3be63ee8146186d695127610fe 100644 (file)
@@ -428,16 +428,16 @@ const char *service_servicetype_txt(service_t *t);
 static inline uint16_t service_id16(void *t)
   { return ((service_t *)t)->s_components.set_service_id; }
 
-int service_is_sdtv(service_t *t);
-int service_is_uhdtv(service_t *t);
-int service_is_hdtv(service_t *t);
-int service_is_radio(service_t *t);
-int service_is_other(service_t *t);
+int service_is_sdtv(const service_t *t);
+int service_is_uhdtv(const service_t *t);
+int service_is_hdtv(const service_t *t);
+int service_is_radio(const service_t *t);
+int service_is_other(const service_t *t);
 
-static inline int service_is_tv( service_t *s)
+static inline int service_is_tv( const service_t *s)
   { return service_is_hdtv(s) || service_is_sdtv(s) || service_is_uhdtv(s); }
 
-int service_is_encrypted ( service_t *t );
+int service_is_encrypted ( const service_t *t );
 
 void service_set_enabled ( service_t *t, int enabled, int _auto );