]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
hdhomerun: Add HDHomeRun server support for LiveTV only (#4461)
authorE.Smith <31170571+azlm8t@users.noreply.github.com>
Wed, 10 Jun 2020 17:45:04 +0000 (18:45 +0100)
committerFlole998 <Flole998@users.noreply.github.com>
Sat, 9 Dec 2023 20:16:23 +0000 (21:16 +0100)
Co-authored-by: "E.Smith" <31170571+azlm8t@users.noreply.github.com>
Co-authored-by: Christian Kündig <christian@kuendig.info>
configure
src/config.c [changed mode: 0644->0755]
src/config.h [changed mode: 0644->0755]
src/htsmsg.c
src/htsmsg.h
src/htsmsg_json.h
src/webui/webui.c

index 7df3ea9aa901c3e06cc86f79384a504df2f3c1b1..0d729023cdae564e430bbc218a9d677912ab7e27 100755 (executable)
--- a/configure
+++ b/configure
@@ -28,6 +28,7 @@ OPTIONS=(
   "satip_server:yes"
   "satip_client:yes"
   "hdhomerun_client:no"
+  "hdhomerun_server:yes"
   "hdhomerun_static:yes"
   "iptv:yes"
   "tsfile:yes"
old mode 100644 (file)
new mode 100755 (executable)
index eaeba32..dfef3a1
@@ -2577,6 +2577,69 @@ const idclass_t config_class = {
       .opts   = PO_HIDDEN | PO_EXPERT,
       .group  = 6
     },
+    {
+      .type   = PT_U32,
+      .id     = "hdhomerun_server_tuner_count",
+      .name   = N_("Number of tuners to export for HDHomeRun Server Emulation"),
+      .desc   = N_("When Tvheadend is acting as an HDHomeRun Server "
+                   "(emulating an HDHomeRun device for downstream "
+                   "media devices to stream Live TV) then "
+                   "we tell clients that we have this number of tuners. "
+                   "This is necessary since some clients artificially limit "
+                   "connections based on tuner count, even though several "
+                   "channels may share a multiplex on one tuner. "
+                   "The HDHomeRun interface can not distinguish between "
+                   "different types of tuner in a mixed system with "
+                   "satellite, aerial and cable. "
+                   "The actual number or types of tuners used by Tvheadend is "
+                   "not affected by this value.  Tvheadend will "
+                   "allocate tuners automatically.  "
+                   "Set to zero for Tvheadend to use a default value."
+                  ),
+      .off    = offsetof(config_t, hdhomerun_server_tuner_count),
+      .opts   = PO_EXPERT
+#if !ENABLE_HDHOMERUN_SERVER
+      | PO_PHIDDEN
+#endif
+      ,
+      .group  = 6,
+    },
+    {
+      .type   = PT_STR,
+      .id     = "hdhomerun_server_model_name",
+      .name   = N_("Tvheadend model name for HDHomeRun Server Emulation"),
+      .desc   = N_("When Tvheadend is acting as an HDHomeRun Server "
+                   "(emulating an HDHomeRun device for downstream "
+                   "media devices to stream Live TV) then "
+                   "we use this as the type of HDHomeRun model number "
+                   "that we send to clients.  Some clients may require "
+                   "a specific model number to work.  Leave blank "
+                   "for Tvheadend to use a default."
+                  ),
+      .off    = offsetof(config_t, hdhomerun_server_model_name),
+      .opts   = PO_EXPERT
+#if !ENABLE_HDHOMERUN_SERVER
+      | PO_PHIDDEN
+#endif
+      ,
+      .group  = 6,
+    },
+    {
+      .type   = PT_BOOL,
+      .id     = "hdhomerun_server_enable",
+      .name   = N_("Enable HDHomeRun Server Emulation"),
+      .desc   = N_("Enable the Tvheadend server to emulate "
+                   "an HDHomeRun server.  This allows LiveTV "
+                   "to be used on some media servers."
+                  ),
+      .off    = offsetof(config_t, hdhomerun_server_enable),
+      .opts   = PO_EXPERT
+#if !ENABLE_HDHOMERUN_SERVER
+      | PO_PHIDDEN
+#endif
+,
+      .group  = 6
+    },
     {
       .type   = PT_STR,
       .id     = "http_user_agent",
old mode 100644 (file)
new mode 100755 (executable)
index cb5b294..8cdb54d
@@ -75,6 +75,9 @@ typedef struct config {
   char *hdhomerun_ip;
   char *local_ip;
   int local_port;
+  uint32_t hdhomerun_server_tuner_count;
+  char *hdhomerun_server_model_name;
+  int hdhomerun_server_enable;
 } config_t;
 
 extern const idclass_t config_class;
index 5466cc1361d87951b4ba3e5e30e8ce9cd36a186b..74a8a0300a13f6b8ba1f2bc3c446edcf34f7fd61 100644 (file)
@@ -1589,3 +1589,63 @@ htsmsg_remove_string_from_list(htsmsg_t *list, const char *str)
   }
   return 0;
 }
+
+
+/*
+ * Based on htsbuf_vqprintf, but can't easily share code since we rely
+ * on stack allocations.
+*/
+static void
+htsmsg_add_str_ap(htsmsg_t *msg, const char *name, const char *fmt, va_list ap0)
+{
+  // First try to format it on-stack
+  va_list ap;
+  int n;
+  size_t size;
+  char buf[100], *p, *np;
+
+  va_copy(ap, ap0);
+  n = vsnprintf(buf, sizeof(buf), fmt, ap);
+  va_end(ap);
+  if(n > -1 && n < sizeof(buf)) {
+    htsmsg_add_str(msg, name, buf);
+    return;
+  }
+
+  // Else, do allocations
+  size = sizeof(buf) * 2;
+
+  p = malloc(size);
+  while (1) {
+    // Try to print in the allocated space. 
+    va_copy(ap, ap0);
+    n = vsnprintf(p, size, fmt, ap);
+    va_end(ap);
+    if(n > -1 && n < size) {
+      htsmsg_add_str(msg, name, p);
+      // Copy taken by htsmsg_add_str.
+      free (p);
+      return;
+    }
+    // Else try again with more space. 
+    if (n > -1)   // glibc 2.1
+      size = n+1; // precisely what is needed
+    else          // glibc 2.0 
+      size *= 2;  // twice the old size 
+    if ((np = realloc (p, size)) == NULL) {
+      free(p);
+      abort();
+    } else {
+      p = np;
+    }
+  }
+}
+
+void
+htsmsg_add_str_printf(htsmsg_t *msg, const char *name, const char *fmt, ...)
+{
+  va_list ap;
+  va_start(ap, fmt);
+  htsmsg_add_str_ap(msg, name, fmt, ap);
+  va_end(ap);
+}
index 3403dbf1f9ee817b1c94e85f69e89f6b96a72155..82787d4cc4d18436653ab0c19fc2e49ee930a013 100644 (file)
@@ -213,6 +213,13 @@ void htsmsg_add_str_alloc(htsmsg_t *msg, const char *name, char *str);
  */
 void htsmsg_add_str_exclusive(htsmsg_t *msg, const char *str);
 
+/**
+ * Add a string using printf-style for the value.
+ */
+void
+htsmsg_add_str_printf(htsmsg_t *msg, const char *name, const char *fmt, ...)
+  __attribute__((format(printf,3,4)));;
+
 /**
  * Add/update a string field
  */
index fae3c45719ae83007e71e77891381059028c7733..ae7cf4cf26406f8a640fe0662cc3bb897a4b089c 100644 (file)
@@ -29,6 +29,7 @@ htsmsg_t *htsmsg_json_deserialize(const char *src);
 
 void htsmsg_json_serialize(htsmsg_t *msg, htsbuf_queue_t *hq, int pretty);
 
+__attribute__((warn_unused_result))
 char *htsmsg_json_serialize_to_str(htsmsg_t *msg, int pretty);
 
 struct rstr *htsmsg_json_serialize_to_rstr(htsmsg_t *msg, const char *prefix);
index 59025d8d9b3358de5c564ff5b679f3844e994779..68a15f621711b5f9f45ba526767738afc1087a7e 100755 (executable)
@@ -26,6 +26,7 @@
 #include "tcp.h"
 #include "udp_stream.h"
 #include "webui.h"
+#include "htsmsg_json.h"
 #include "dvr/dvr.h"
 #include "filebundle.h"
 #include "streaming.h"
@@ -1861,6 +1862,277 @@ page_play_auth(http_connection_t *hc, const char *remain, void *opaque)
   return page_play_(hc, remain, opaque, URLAUTH_CODE);
 }
 
+
+
+#if ENABLE_HDHOMERUN_SERVER
+/*
+ * Get the HDHomerun server name.
+ */ 
+static const char *hdhomerun_get_server_name(void)
+{
+  return config.server_name ?: "Tvheadend";
+}
+
+/**
+ * Our unique device id is calculated from our server's name
+ * in the general config tab.
+ */
+static uint32_t hdhomerun_get_deviceid(void)
+{
+  const char *server_name = hdhomerun_get_server_name();
+  const uint32_t deviceid = tvh_crc32((const uint8_t*)server_name, strlen(server_name), 0);
+  return deviceid;
+}
+
+/*
+ * Get the model name, defaulting to a commonly used version.
+ */ 
+static const char *hdhomerun_get_model_name(void)
+{
+  if (config.hdhomerun_server_model_name && !strempty(config.hdhomerun_server_model_name))
+    return config.hdhomerun_server_model_name;
+  else
+    return "HDTC-2US";
+}
+
+
+/**
+ * Check if the request has streaming rights. For almost all HDHomerun clients 
+ * this will require setting up an access rule without a username. but with IP matching.
+ * @param fail_log_reason Log this reason if permissions fail.
+ * @return Permission for the verified user (caller owns the memory) or NULL.
+ */
+__attribute__((warn_unused_result))
+static access_t *hdhomerun_verify_user_permission(const http_connection_t *hc,
+                                                  const char *fail_log_reason)
+{
+  // HDHomerun emulation not explicitly enabled?  Then all calls fail. 
+  if (!config.hdhomerun_server_enable) {
+    tvhwarn(LS_WEBUI, "hdhomerun server not enabled but received request [%s]",
+            fail_log_reason?:"");
+    return NULL;
+  }
+
+  const char *hdhr_user = hc->hc_username ?: "";
+  access_t *perm = hc->hc_access;
+
+  if (access_verify2(perm, ACCESS_STREAMING)) {
+    // Acces verification Failed 
+    tvhwarn(LS_WEBUI, "hdhomerun server received request but no streaming permission for user [%s] [%d] [%s]",
+            hdhr_user ?: "<none>",
+            perm? perm->aa_rights : 0,
+            fail_log_reason?:"");
+    return NULL;
+  } else {
+    return perm;
+  }
+}
+
+
+/**
+ * Return the discovery information for HDHomeRun to give clients
+ * details of how to access the lineup.
+ */
+static int
+hdhomerun_server_discover(http_connection_t *hc, const char *remain, void *opaque)
+{
+  access_t *perm = hdhomerun_verify_user_permission(hc, "discover");
+  if (!perm)
+    return http_noaccess_code(hc);
+
+  char http_ip[128];
+  htsbuf_queue_t *hq = &hc->hc_reply;
+  const char *server_name = hdhomerun_get_server_name();
+  const uint32_t deviceid = hdhomerun_get_deviceid();
+
+  tcp_get_str_from_ip(hc->hc_self, http_ip, sizeof(http_ip));
+
+  // The contents below for the discovery message are based on jkaberg/tvhProxy 
+  htsmsg_t *msg = htsmsg_create_map();
+  htsmsg_add_str(msg, "FriendlyName", server_name);
+  htsmsg_add_str(msg,"FirmwareVersion", tvheadend_version);
+  // Currently hardcoded until we encounter a client that has a problem.
+  htsmsg_add_str(msg, "FirmwareName", "hdhomerun_atsc");
+  // We use same value for model name/number to avoid too many user
+  // configuration options.
+  htsmsg_add_str(msg, "ModelNumber", hdhomerun_get_model_name());
+  htsmsg_add_str(msg, "Manufacturer", "Tvheadend");
+  // Random string, but has to be fixed length.
+  htsmsg_add_str(msg, "DeviceAuth", "3xw5UaJXhVShHEBoy76FuYQi");
+  htsmsg_add_str_printf(msg, "BaseURL", "http://%s:%u", http_ip, tvheadend_webui_port);
+  htsmsg_add_str_printf(msg, "DeviceID", "%08X", deviceid);
+  htsmsg_add_str_printf(msg, "LineupURL", "http://%s:%u/lineup.json", http_ip, tvheadend_webui_port);
+
+  // If user has not explicitly set a count then we use a default.
+  // The actual number of tuners is unknown since we allow multiplex
+  // sharing and some channels may actually be iptv channels so not
+  // use a tuner at all.
+  htsmsg_add_u32(msg, "TunerCount", config.hdhomerun_server_tuner_count ?: 6);
+
+  char *json = htsmsg_json_serialize_to_str(msg, 1);
+  htsbuf_append_str(hq, json);
+  free(json);
+  http_output_content(hc, "application/json");
+  return 0;
+}
+
+
+/**
+ * Return the channel lineup for HDHomeRun
+ */
+static int
+hdhomerun_server_lineup(http_connection_t *hc, const char *remain, void *opaque)
+{
+  access_t *perm = hdhomerun_verify_user_permission(hc, "lineup");
+  if (!perm)
+    return http_noaccess_code(hc);
+
+  htsbuf_queue_t *hq = &hc->hc_reply;
+  channel_t *ch;
+  const char *name;
+  const char *blank;
+  const char *chnum_str;
+  const int use_auth = perm && perm->aa_auth && !strempty(perm->aa_auth);
+  char buf1[128], chnum[32], ubuf[UUID_HEX_SIZE];
+  char url[1024];
+  char http_ip[128];
+  // We use the UI flags to determine if we should include channel
+  // numbers/sources in the name.  This can help distinguish channels
+  // when you have multiple different sources of the same channel such
+  // as satellite and aerial.
+  const int flags =
+    (config.chname_num ? CHANNEL_ENAME_NUMBERS : 0) |
+    (config.chname_src ? CHANNEL_ENAME_SOURCES : 0);
+  int is_first = 1;
+
+  tcp_get_str_from_ip(hc->hc_self, http_ip, sizeof(http_ip));
+  blank = tvh_gettext_lang(perm->aa_lang_ui, channel_blank_name);
+  htsbuf_append_str(hq, "[");
+  tvh_mutex_lock(&global_lock);
+  CHANNEL_FOREACH(ch) {
+    if (!channel_access(ch, perm, 0) || !ch->ch_enabled)
+      continue;
+    if (!is_first)
+      htsbuf_append_str(hq, ", \n");
+    name = channel_get_ename(ch, buf1, sizeof(buf1), blank, flags);
+    htsbuf_append_str(hq, "{ \"GuideName\" : ");
+    htsbuf_append_and_escape_jsonstr(hq, name);
+    htsbuf_append_str(hq, ", \"GuideNumber\" : ");
+    // channel_get_number_as_str returns NULL if no channel number!
+    chnum_str = channel_get_number_as_str(ch, chnum, sizeof(chnum));
+    htsbuf_append_and_escape_jsonstr(hq, chnum_str ? chnum_str : "0");
+    htsbuf_append_str(hq, ", \"URL\" : ");
+    sprintf(url, "http://%s:%u/stream/channel/%s?profile=pass%s%s",
+            http_ip,
+            tvheadend_webui_port,
+            channel_get_uuid(ch, ubuf),
+            use_auth? "&auth=" : "",
+            use_auth ? perm->aa_auth : "");
+    htsbuf_append_and_escape_jsonstr(hq, url);
+    htsbuf_append_str(hq, "}");
+    is_first = 0;
+  }
+  tvh_mutex_unlock(&global_lock);
+  htsbuf_append_str(hq, "]");
+  http_output_content(hc, "application/json");
+  return 0;
+}
+
+
+static int
+hdhomerun_server_lineup_status(http_connection_t *hc, const char *remain, void *opaque)
+{
+  access_t *perm = hdhomerun_verify_user_permission(hc, "lineup_status");
+  if (!perm)
+    return http_noaccess_code(hc);
+
+  htsbuf_queue_t *hq = &hc->hc_reply;
+  // The contents below for the status message are based on jkaberg/tvhProxy.
+  htsbuf_append_str(hq, "{\"ScanInProgress\":0,\"ScanPossible\":0,\"Source\":\"Antenna\",\"SourceList\":[\"Antenna\"]}");
+  http_output_content(hc, "application/json");
+  return 0;
+}
+
+
+
+/**
+ * Some media players ignore the "scan not possible" and do a post to
+ * this function with "?scan=start".
+ *
+ * We currently ignore this request and just return success.
+ * This is because Tvheadend has separate scanning and mapping stages.
+ */
+static int
+hdhomerun_server_lineup_post(http_connection_t *hc, const char *remain, void *opaque)
+{
+  access_t *perm = hdhomerun_verify_user_permission(hc, "lineup_status");
+  if (!perm)
+    return http_noaccess_code(hc);
+
+  // We can't send empty contents since the caller thinks empty (size
+  // 0) is "unknown length" (for streaming data).  So, we'll return an
+  // empty json document.
+  htsbuf_append_str(&hc->hc_reply, "{}");
+  http_output_content(hc, "application/json");
+  return 0;
+}
+
+/**
+ * Needed for some clients.  This contains much the same as discover,
+ * but in xml format.
+ */
+static int
+hdhomerun_server_device_xml(http_connection_t *hc, const char *remain, void *opaque)
+{
+  access_t *perm = hdhomerun_verify_user_permission(hc, "device.xml");
+  if (!perm)
+    return http_noaccess_code(hc);
+
+  const char *server_name = hdhomerun_get_server_name();
+  const char *model_name = hdhomerun_get_model_name();
+  // Need to escape strings in xml 
+  char server_name_escaped[128];
+  char model_name_escaped[128];
+  char http_ip[128];
+  htsbuf_queue_t *hq = &hc->hc_reply;
+  const uint32_t deviceid = hdhomerun_get_deviceid();
+
+  html_escape(server_name_escaped, server_name, sizeof(server_name_escaped));
+  html_escape(model_name_escaped, model_name, sizeof(model_name_escaped));
+
+  // The contents below for the discovery message are based on jkaberg/tvhProxy 
+  tcp_get_str_from_ip(hc->hc_self, http_ip, sizeof(http_ip));
+  htsbuf_qprintf(hq, "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
+                 "<specVersion>"
+                 "<major>1</major>"
+                 "<minor>0</minor>"
+                 "</specVersion>"
+                 "<URLBase>http://%s:%u</URLBase>"
+                 "<device>"
+                 "<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"
+                 "<friendlyName>%s</friendlyName>"
+                 "<manufacturer>Tvheadend</manufacturer>"
+                 "<modelName>%s</modelName>"
+                 "<modelNumber>%s</modelNumber>"
+                 "<serialNumber></serialNumber>"
+                 // Version 5 UUID (random) with top part as server id
+                 "<UDN>uuid:%8.8x-745e-5d9a-8903-4a02327a7e09</UDN>"
+                 "</device>"
+                 "</root>",
+                 http_ip, tvheadend_webui_port,
+                 server_name_escaped,
+                 // We'll use the same for model name and number to
+                 // avoid too much user configuration.  Some clients
+                 // may use the model name to infer characteristics.
+                 model_name_escaped,
+                 model_name_escaped,
+                 deviceid);
+
+  http_output_content(hc, "application/xml");
+  return 0;
+}
+#endif  /* ENABLE_HDHOMERUN_SERVER */
+
 /**
  *
  */
@@ -2398,6 +2670,16 @@ webui_init(int xspf)
   http_path_add("/satip_server", NULL, satip_server_http_page, ACCESS_ANONYMOUS);
 #endif
 
+#if ENABLE_HDHOMERUN_SERVER
+  /* These names are specified in https://info.hdhomerun.com/info/http_api */
+  http_path_add("/discover.json", NULL, hdhomerun_server_discover, ACCESS_ANONYMOUS);
+  http_path_add("/lineup.json", NULL, hdhomerun_server_lineup, ACCESS_ANONYMOUS);
+  /* These names are not specified in the documents but are required to make Plex work. */
+  http_path_add("/lineup_status.json", NULL, hdhomerun_server_lineup_status, ACCESS_ANONYMOUS);
+  http_path_add("/lineup.post", NULL, hdhomerun_server_lineup_post, ACCESS_ANONYMOUS);
+  http_path_add("/device.xml", NULL, hdhomerun_server_device_xml, ACCESS_ANONYMOUS);
+#endif
+
   http_path_add_modify("/play", NULL, page_play, ACCESS_ANONYMOUS, page_play_path_modify5);
   http_path_add_modify("/play/ticket", NULL, page_play_ticket, ACCESS_ANONYMOUS, page_play_path_modify12);
   http_path_add_modify("/play/auth", NULL, page_play_auth, ACCESS_ANONYMOUS, page_play_path_modify10);