From: E.Smith <31170571+azlm8t@users.noreply.github.com> Date: Wed, 10 Jun 2020 17:45:04 +0000 (+0100) Subject: hdhomerun: Add HDHomeRun server support for LiveTV only (#4461) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3dcb7ecf36666dcb43211a84141b1b645c9ca757;p=thirdparty%2Ftvheadend.git hdhomerun: Add HDHomeRun server support for LiveTV only (#4461) Co-authored-by: "E.Smith" <31170571+azlm8t@users.noreply.github.com> Co-authored-by: Christian Kündig --- diff --git a/configure b/configure index 7df3ea9aa..0d729023c 100755 --- 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" diff --git a/src/config.c b/src/config.c old mode 100644 new mode 100755 index eaeba32a9..dfef3a125 --- a/src/config.c +++ b/src/config.c @@ -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", diff --git a/src/config.h b/src/config.h old mode 100644 new mode 100755 index cb5b29404..8cdb54d70 --- a/src/config.h +++ b/src/config.h @@ -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; diff --git a/src/htsmsg.c b/src/htsmsg.c index 5466cc136..74a8a0300 100644 --- a/src/htsmsg.c +++ b/src/htsmsg.c @@ -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); +} diff --git a/src/htsmsg.h b/src/htsmsg.h index 3403dbf1f..82787d4cc 100644 --- a/src/htsmsg.h +++ b/src/htsmsg.h @@ -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 */ diff --git a/src/htsmsg_json.h b/src/htsmsg_json.h index fae3c4571..ae7cf4cf2 100644 --- a/src/htsmsg_json.h +++ b/src/htsmsg_json.h @@ -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); diff --git a/src/webui/webui.c b/src/webui/webui.c index 59025d8d9..68a15f621 100755 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -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 ?: "", + 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, "" + "" + "1" + "0" + "" + "http://%s:%u" + "" + "urn:schemas-upnp-org:device:MediaServer:1" + "%s" + "Tvheadend" + "%s" + "%s" + "" + // Version 5 UUID (random) with top part as server id + "uuid:%8.8x-745e-5d9a-8903-4a02327a7e09" + "" + "", + 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);