Ver código fonte

obs-outputs: Adopt Happy Eyeballs in RTMP

This commit adopts the happy eyeballs utility in the RTMP
output module, replacing the existing TCP connection logic.
James Hurley 2 anos atrás
pai
commit
ba41613ab8

+ 5 - 1
plugins/obs-outputs/CMakeLists.txt

@@ -44,7 +44,11 @@ target_sources(
           "$<$<BOOL:${ENABLE_HEVC}>:rtmp-hevc.c>"
           "$<$<BOOL:${ENABLE_HEVC}>:rtmp-hevc.h>")
 
-target_link_libraries(obs-outputs PRIVATE OBS::libobs MbedTLS::MbedTLS ZLIB::ZLIB)
+if(NOT TARGET happy-eyeballs)
+  add_subdirectory("${CMAKE_SOURCE_DIR}/deps/happy-eyeballs" "${CMAKE_BINARY_DIR}/deps/happy-eyeballs")
+endif()
+
+target_link_libraries(obs-outputs PRIVATE OBS::libobs OBS::happy-eyeballs MbedTLS::MbedTLS ZLIB::ZLIB)
 
 target_compile_definitions(obs-outputs PRIVATE USE_MBEDTLS CRYPTO)
 

+ 1 - 1
plugins/obs-outputs/cmake/legacy.cmake

@@ -51,7 +51,7 @@ if(ENABLE_HEVC)
   target_sources(obs-outputs PRIVATE rtmp-hevc.c rtmp-hevc.h)
 endif()
 
-target_link_libraries(obs-outputs PRIVATE OBS::libobs)
+target_link_libraries(obs-outputs PRIVATE OBS::libobs OBS::happy-eyeballs)
 
 set_target_properties(obs-outputs PROPERTIES FOLDER "plugins" PREFIX "")
 

+ 7 - 0
plugins/obs-outputs/data/locale/en-US.ini

@@ -7,6 +7,11 @@ FLVOutput="FLV File Output"
 FLVOutput.FilePath="File Path"
 Default="Default"
 
+IPFamily="IP Address Family"
+IPFamily.Both="IPv4 and IPv6 (Default)"
+IPFamily.V4Only="IPv4 Only"
+IPFamily.V6Only="IPv6 Only"
+
 ConnectionTimedOut="The connection timed out. Make sure you've configured a valid streaming service and no firewall is blocking the connection."
 PermissionDenied="The connection was blocked. Check your firewall / anti-virus settings to make sure OBS is allowed full internet access."
 ConnectionAborted="The connection was aborted. This usually indicates internet connection problems between you and the streaming service."
@@ -15,6 +20,8 @@ HostNotFound="Hostname not found. Make sure you entered a valid streaming server
 NoData="Hostname found, but no data of the requested type. This can occur if you have bound to an IPv6 address and your streaming service only has IPv4 addresses (see Settings → Advanced)."
 AddressNotAvailable="Address not available. You may have tried to bind to an invalid IP address (see Settings → Advanced)."
 SSLCertVerifyFailed="The RTMP server sent an invalid SSL certificate."
+InvalidParameter="Invalid connection parameters. Check that the streaming service address is correct."
+NoRoute="Error reaching host. Make sure that the interface you have bound can access the internet and that the streaming service supports the address family you selected (see Settings → Advanced)."
 
 FTLStream="FTL Stream"
 FTLStream.PeakBitrate="Peak Bitrate"

+ 106 - 82
plugins/obs-outputs/librtmp/rtmp.c

@@ -32,6 +32,7 @@
 #include "rtmp_sys.h"
 #include "log.h"
 
+#include "happy-eyeballs.h"
 #include <util/platform.h>
 
 #if !defined(MSG_NOSIGNAL)
@@ -344,7 +345,7 @@ RTMP_TLS_LoadCerts(RTMP *r) {
     }
 
     CFRelease(result);
-    
+
 #elif defined(__linux__)
     if (mbedtls_x509_crt_parse_path(chain, "/etc/ssl/certs/") < 0) {
         RTMP_Log(RTMP_LOGERROR, "mbedtls_x509_crt_parse_path: Couldn't parse "
@@ -744,6 +745,29 @@ int RTMP_AddStream(RTMP *r, const char *playpath)
     return idx;
 }
 
+/* Returns true if the string needs to be freed */
+static char* get_hostname(AVal *host, bool *should_free)
+{
+    if (should_free == NULL)
+        return NULL;
+
+    if (host->av_val[host->av_len] || host->av_val[0] == '[')
+    {
+        int v6 = host->av_val[0] == '[';
+        char* hostname = malloc(host->av_len+1 - v6 * 2);
+        if (hostname != NULL)
+        {
+            memcpy(hostname, host->av_val + v6, host->av_len - v6 * 2);
+            hostname[host->av_len - v6 * 2] = '\0';
+            *should_free = TRUE;
+        }
+        return hostname;
+    }
+
+    *should_free = FALSE;
+    return host->av_val;
+}
+
 static int
 add_addr_info(struct sockaddr_storage *service, socklen_t *addrlen, AVal *host, int port, socklen_t addrlen_hint, int *socket_error)
 {
@@ -845,68 +869,28 @@ finish:
 #define E_TIMEDOUT     WSAETIMEDOUT
 #define E_CONNREFUSED  WSAECONNREFUSED
 #define E_ACCES        WSAEACCES
+#define E_INVAL        WSAEINVAL
+#define E_HOSTUNREACH  WSAEHOSTUNREACH
 #else
 #define E_TIMEDOUT     ETIMEDOUT
 #define E_CONNREFUSED  ECONNREFUSED
 #define E_ACCES        EACCES
+#define E_INVAL        EINVAL
+#define E_HOSTUNREACH  EHOSTUNREACH
 #endif
 
 int
-RTMP_Connect0(RTMP *r, struct sockaddr * service, socklen_t addrlen)
+RTMP_Connect0(RTMP *r, SOCKET socket_fd)
 {
     int on = 1;
     r->m_sb.sb_timedout = FALSE;
     r->m_pausing = 0;
     r->m_fDuration = 0.0;
 
-    //best to be explicit, we need overlapped socket
-#ifdef _WIN32
-    r->m_sb.sb_socket = WSASocket(service->sa_family, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
-#else
-    r->m_sb.sb_socket = socket(service->sa_family, SOCK_STREAM, IPPROTO_TCP);
-#endif
+    r->m_sb.sb_socket = socket_fd;
 
     if (r->m_sb.sb_socket != INVALID_SOCKET)
     {
-#ifndef _WIN32
-#ifdef SO_NOSIGPIPE
-        setsockopt(r->m_sb.sb_socket, SOL_SOCKET, SO_NOSIGPIPE, &(int){ 1 }, sizeof(int));
-#endif
-#endif
-        if(r->m_bindIP.addrLen)
-        {
-            if (bind(r->m_sb.sb_socket, (const struct sockaddr *)&r->m_bindIP.addr, r->m_bindIP.addrLen) < 0)
-            {
-                int err = GetSockError();
-                RTMP_Log(RTMP_LOGERROR, "%s, failed to bind socket: %s (%d)",
-                         __FUNCTION__, socketerror(err), err);
-                r->last_error_code = err;
-                RTMP_Close(r);
-                return FALSE;
-            }
-        }
-
-        uint64_t connect_start = os_gettime_ns();
-
-        if (connect(r->m_sb.sb_socket, service, addrlen) < 0)
-        {
-            int err = GetSockError();
-            if (err == E_CONNREFUSED)
-                RTMP_Log(RTMP_LOGERROR, "%s is offline. Try a different server (ECONNREFUSED).", r->Link.hostname.av_val);
-            else if (err == E_ACCES)
-                RTMP_Log(RTMP_LOGERROR, "The connection is being blocked by a firewall or other security software (EACCES).");
-            else if (err == E_TIMEDOUT)
-                RTMP_Log(RTMP_LOGERROR, "The connection timed out. Try a different server, or check that the connection is not being blocked by a firewall or other security software (ETIMEDOUT).");
-            else
-                RTMP_Log(RTMP_LOGERROR, "%s, failed to connect socket: %s (%d)",
-                     __FUNCTION__, socketerror(err), err);
-            r->last_error_code = err;
-            RTMP_Close(r);
-            return FALSE;
-        }
-
-        r->connect_time_ms = (int)((os_gettime_ns() - connect_start) / 1000000);
-
         if (r->Link.socksport)
         {
             RTMP_Log(RTMP_LOGDEBUG, "%s ... SOCKS negotiation", __FUNCTION__);
@@ -1059,58 +1043,98 @@ RTMP_Connect1(RTMP *r, RTMPPacket *cp)
 int
 RTMP_Connect(RTMP *r, RTMPPacket *cp)
 {
-#ifdef _WIN32
-    HOSTENT *h;
-#endif
-    struct sockaddr_storage service;
-    socklen_t addrlen = 0;
-    socklen_t addrlen_hint = 0;
-    int socket_error = 0;
+    struct happy_eyeballs_ctx* happy_ctx = NULL;
+    bool free_hostname = FALSE;
+    char *hostname = NULL;
+    int port = 0;
+    int result = FALSE;
 
-    if (!r->Link.hostname.av_len)
-        return FALSE;
-
-#ifdef _WIN32
-    //COMODO security software sandbox blocks all DNS by returning "host not found"
-    h = gethostbyname("localhost");
-    if (!h && GetLastError() == WSAHOST_NOT_FOUND)
+    int he_result = happy_eyeballs_create(&happy_ctx);
+    if (he_result != 0)
     {
-        r->last_error_code = WSAHOST_NOT_FOUND;
-        RTMP_Log(RTMP_LOGERROR, "RTMP_Connect: Connection test failed. This error is likely caused by Comodo Internet Security running OBS in sandbox mode. Please add OBS to the Comodo automatic sandbox exclusion list, restart OBS and try again (11001).");
-        return FALSE;
+        /* did not successfully create the happy eyeballs context */
+        r->last_error_code = -he_result;
+        goto fail;
     }
-#endif
-
-    memset(&service, 0, sizeof(service));
-
-    if (r->m_bindIP.addrLen)
-        addrlen_hint = r->m_bindIP.addrLen;
 
     if (r->Link.socksport)
     {
         /* Connect via SOCKS */
-        if (!add_addr_info(&service, &addrlen, &r->Link.sockshost, r->Link.socksport, addrlen_hint, &socket_error))
-        {
-            r->last_error_code = socket_error;
-            return FALSE;
-        }
+        hostname = get_hostname(&r->Link.sockshost, &free_hostname);
+        port = r->Link.socksport;
     }
     else
     {
         /* Connect directly */
-        if (!add_addr_info(&service, &addrlen, &r->Link.hostname, r->Link.port, addrlen_hint, &socket_error))
+        hostname = get_hostname(&r->Link.hostname, &free_hostname);
+        port = r->Link.port;
+    }
+
+    /* Set local bind address (if present) */
+    happy_eyeballs_set_bind_addr(happy_ctx, r->m_bindIP.addrLen, &r->m_bindIP.addr);
+
+    /* Attempt connection */
+    he_result = happy_eyeballs_connect(happy_ctx, hostname, port);
+    if (he_result == EAGAIN)
+    {
+        /* Connect returned with the connection process ongoing, let's wait for a few more seconds... */
+        he_result = happy_eyeballs_timedwait_default(happy_ctx);
+    }
+
+    if (he_result == -E_INVAL)
+    {
+        /* Parameter error */
+        r->last_error_code = E_INVAL;
+        RTMP_Log(RTMP_LOGERROR, "Invalid connection parameters. Try to make sure you're using a valid server address and port.");
+        goto fail;
+
+    }
+    else if (he_result != 0)
+    {
+        /* Error while connecting */
+        int err = happy_eyeballs_get_error_code(happy_ctx);
+        if (err == E_CONNREFUSED)
+            RTMP_Log(RTMP_LOGERROR, "%s is offline. Try a different server (ECONNREFUSED).", r->Link.hostname.av_val);
+        else if (err == E_ACCES)
+            RTMP_Log(RTMP_LOGERROR, "The connection is being blocked by a firewall or other security software (EACCES).");
+        else if (err == E_TIMEDOUT)
+            RTMP_Log(RTMP_LOGERROR, "The connection timed out. Try a different server, or check that the connection is not being blocked by a firewall or other security software (ETIMEDOUT).");
+        else if (r->m_bindIP.addrLen > 0)
         {
-            r->last_error_code = socket_error;
-            return FALSE;
+            /* There are several different errors that platform network implementations can
+             * emit when a user attempts to connect to e.g. IPv6 on a device where it is
+             * disabled (EINVAL, EHOSTUNREACH) or vice-versa. Squash this down to EHOSTUNREACH
+             * in order to emit a more user-friendly error. */
+            RTMP_Log(RTMP_LOGERROR, "Invalid socket settings: %s (%d). Are you trying to use IPv6 on an IPv4-only interface?", socketerror(err), err);
+            err = E_HOSTUNREACH;
         }
+        else
+            RTMP_Log(RTMP_LOGERROR, "%s, failed to connect socket: %s (%d)",
+                    __FUNCTION__, socketerror(err), err);
+        r->last_error_code = err;
+        goto fail;
     }
 
-    if (!RTMP_Connect0(r, (struct sockaddr *)&service, addrlen))
-        return FALSE;
+    r->connect_time_ms = (int)(happy_eyeballs_get_connection_time_ns(happy_ctx) / 1000000);
+
+    /* Successful connection */
+    SOCKET socket_fd = happy_eyeballs_get_socket_fd(happy_ctx);
+    result = RTMP_Connect0(r, socket_fd);
+    if (result)
+    {
+        r->m_bSendCounter = TRUE;
+        result = RTMP_Connect1(r, cp);
+    }
 
-    r->m_bSendCounter = TRUE;
+fail:
+    if (!result)
+        RTMP_Close(r);
+    if (happy_ctx)
+        happy_eyeballs_destroy(happy_ctx);
+    if (free_hostname)
+        free(hostname);
 
-    return RTMP_Connect1(r, cp);
+    return result;
 }
 
 static int

+ 1 - 1
plugins/obs-outputs/librtmp/rtmp.h

@@ -488,7 +488,7 @@ extern "C"
 
     int RTMP_Connect(RTMP *r, RTMPPacket *cp);
     struct sockaddr;
-    int RTMP_Connect0(RTMP *r, struct sockaddr *svc, socklen_t addrlen);
+    int RTMP_Connect0(RTMP *r, SOCKET socket_fd);
     int RTMP_Connect1(RTMP *r, RTMPPacket *cp);
 
     int RTMP_ReadPacket(RTMP *r, RTMPPacket *packet);

+ 41 - 0
plugins/obs-outputs/rtmp-stream.c

@@ -528,6 +528,12 @@ static void set_output_error(struct rtmp_stream *stream)
 	case WSAEADDRNOTAVAIL:
 		msg = obs_module_text("AddressNotAvailable");
 		break;
+	case WSAEINVAL:
+		msg = obs_module_text("InvalidParameter");
+		break;
+	case WSAEHOSTUNREACH:
+		msg = obs_module_text("NoRoute");
+		break;
 	}
 #else
 	switch (stream->rtmp.last_error_code) {
@@ -552,6 +558,12 @@ static void set_output_error(struct rtmp_stream *stream)
 	case EADDRNOTAVAIL:
 		msg = obs_module_text("AddressNotAvailable");
 		break;
+	case EINVAL:
+		msg = obs_module_text("InvalidParameter");
+		break;
+	case EHOSTUNREACH:
+		msg = obs_module_text("NoRoute");
+		break;
 	}
 #endif
 
@@ -1195,6 +1207,10 @@ static int try_connect(struct rtmp_stream *stream)
 		}
 	}
 
+	// Only use the IPv4 / IPv6 hint if a binding address isn't specified.
+	if (stream->rtmp.m_bindIP.addrLen == 0)
+		stream->rtmp.m_bindIP.addrLen = stream->addrlen_hint;
+
 	RTMP_AddStream(&stream->rtmp, stream->key.array);
 
 	stream->rtmp.m_outChunkSize = 4096;
@@ -1223,6 +1239,7 @@ static bool init_connect(struct rtmp_stream *stream)
 	obs_service_t *service;
 	obs_data_t *settings;
 	const char *bind_ip;
+	const char *ip_family;
 	int64_t drop_p;
 	int64_t drop_b;
 	uint32_t caps;
@@ -1309,6 +1326,18 @@ static bool init_connect(struct rtmp_stream *stream)
 	bind_ip = obs_data_get_string(settings, OPT_BIND_IP);
 	dstr_copy(&stream->bind_ip, bind_ip);
 
+	// Check that we have an IP Family set and that the setting length
+	// is 4 characters long so we don't capture ie. IPv4+IPv6
+	ip_family = obs_data_get_string(settings, OPT_IP_FAMILY);
+	if (ip_family != NULL && strlen(ip_family) == 4) {
+		socklen_t len = 0;
+		if (strncmp(ip_family, "IPv6", 4) == 0)
+			len = sizeof(struct sockaddr_in6);
+		else if (strncmp(ip_family, "IPv4", 4) == 0)
+			len = sizeof(struct sockaddr_in);
+		stream->addrlen_hint = len;
+	}
+
 #ifdef _WIN32
 	stream->new_socket_loop =
 		obs_data_get_bool(settings, OPT_NEWSOCKETLOOP_ENABLED);
@@ -1723,6 +1752,18 @@ static obs_properties_t *rtmp_stream_properties(void *unused)
 				   200, 10000, 100);
 	obs_property_int_set_suffix(p, " ms");
 
+	p = obs_properties_add_list(props, OPT_IP_FAMILY,
+				    obs_module_text("IPFamily"),
+				    OBS_COMBO_TYPE_LIST,
+				    OBS_COMBO_FORMAT_STRING);
+
+	obs_property_list_add_string(p, obs_module_text("IPFamily.Both"),
+				     "IPv4+IPv6");
+	obs_property_list_add_string(p, obs_module_text("IPFamily.V4Only"),
+				     "IPv4");
+	obs_property_list_add_string(p, obs_module_text("IPFamily.V6Only"),
+				     "IPv6");
+
 	p = obs_properties_add_list(props, OPT_BIND_IP,
 				    obs_module_text("RTMPStream.BindIP"),
 				    OBS_COMBO_TYPE_LIST,

+ 3 - 1
plugins/obs-outputs/rtmp-stream.h

@@ -28,6 +28,7 @@
 #define OPT_PFRAME_DROP_THRESHOLD "pframe_drop_threshold_ms"
 #define OPT_MAX_SHUTDOWN_TIME_SEC "max_shutdown_time_sec"
 #define OPT_BIND_IP "bind_ip"
+#define OPT_IP_FAMILY "ip_family"
 #define OPT_NEWSOCKETLOOP_ENABLED "new_socket_loop_enabled"
 #define OPT_LOWLATENCY_ENABLED "low_latency_mode_enabled"
 #define OPT_METADATA_MULTITRACK "metadata_multitrack"
@@ -81,6 +82,7 @@ struct rtmp_stream {
 	struct dstr username, password;
 	struct dstr encoder_name;
 	struct dstr bind_ip;
+	socklen_t addrlen_hint; /* hint IPv4 vs IPv6 */
 
 	/* frame drop variables */
 	int64_t drop_threshold_usec;
@@ -136,7 +138,7 @@ void *socket_thread_windows(void *data);
 #endif
 
 /* Adapted from FFmpeg's libavutil/pixfmt.h
- * 
+ *
  * Renamed to make it apparent that these are not imported as this module does
  * not use or link against FFmpeg.
  */