/* GStreamer * Copyright (C) 2017 Matthew Waters * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "nice.h" #include "nicestream.h" /* libnice */ #include #ifndef NICE_CHECK_VERSION #define NICE_CHECK_VERSION(major,minor,micro) \ (NICE_VERSION_MAJOR > (major) || \ (NICE_VERSION_MAJOR == (major) && NICE_VERSION_MINOR > (minor)) || \ (NICE_VERSION_MAJOR == (major) && NICE_VERSION_MINOR == (minor) && \ NICE_VERSION_MICRO >= (micro))) #endif /* XXX: * * - are locally generated remote candidates meant to be readded to libnice? */ static GstUri *_validate_turn_server (GstWebRTCNice * ice, const gchar * s); #define GST_CAT_DEFAULT gst_webrtc_nice_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); enum { PROP_0, PROP_AGENT, PROP_ICE_TCP, PROP_ICE_UDP, PROP_MIN_RTP_PORT, PROP_MAX_RTP_PORT, }; struct _GstWebRTCNicePrivate { NiceAgent *nice_agent; GArray *nice_stream_map; GThread *thread; GMainContext *main_context; GMainLoop *loop; GMutex lock; GCond cond; GstWebRTCICEOnCandidateFunc on_candidate; gpointer on_candidate_data; GDestroyNotify on_candidate_notify; GstUri *stun_server; GstUri *turn_server; GHashTable *turn_servers; }; #define gst_webrtc_nice_parent_class parent_class G_DEFINE_TYPE_WITH_CODE (GstWebRTCNice, gst_webrtc_nice, GST_TYPE_WEBRTC_ICE, G_ADD_PRIVATE (GstWebRTCNice) GST_DEBUG_CATEGORY_INIT (gst_webrtc_nice_debug, "webrtcnice", 0, "webrtcnice");); static gboolean _unlock_pc_thread (GMutex * lock) { g_mutex_unlock (lock); return G_SOURCE_REMOVE; } static gpointer _gst_nice_thread (GstWebRTCNice * ice) { g_mutex_lock (&ice->priv->lock); ice->priv->main_context = g_main_context_new (); ice->priv->loop = g_main_loop_new (ice->priv->main_context, FALSE); g_cond_broadcast (&ice->priv->cond); g_main_context_invoke (ice->priv->main_context, (GSourceFunc) _unlock_pc_thread, &ice->priv->lock); g_main_loop_run (ice->priv->loop); g_mutex_lock (&ice->priv->lock); g_main_context_unref (ice->priv->main_context); ice->priv->main_context = NULL; g_main_loop_unref (ice->priv->loop); ice->priv->loop = NULL; g_cond_broadcast (&ice->priv->cond); g_mutex_unlock (&ice->priv->lock); return NULL; } static void _start_thread (GstWebRTCNice * ice) { g_mutex_lock (&ice->priv->lock); ice->priv->thread = g_thread_new (GST_OBJECT_NAME (ice), (GThreadFunc) _gst_nice_thread, ice); while (!ice->priv->loop) g_cond_wait (&ice->priv->cond, &ice->priv->lock); g_mutex_unlock (&ice->priv->lock); } static void _stop_thread (GstWebRTCNice * ice) { g_mutex_lock (&ice->priv->lock); g_main_loop_quit (ice->priv->loop); while (ice->priv->loop) g_cond_wait (&ice->priv->cond, &ice->priv->lock); g_mutex_unlock (&ice->priv->lock); g_thread_unref (ice->priv->thread); } struct NiceStreamItem { guint session_id; guint nice_stream_id; GstWebRTCICEStream *stream; }; /* TRUE to continue, FALSE to stop */ typedef gboolean (*NiceStreamItemForeachFunc) (struct NiceStreamItem * item, gpointer user_data); static void _nice_stream_item_foreach (GstWebRTCNice * ice, NiceStreamItemForeachFunc func, gpointer data) { int i, len; len = ice->priv->nice_stream_map->len; for (i = 0; i < len; i++) { struct NiceStreamItem *item = &g_array_index (ice->priv->nice_stream_map, struct NiceStreamItem, i); if (!func (item, data)) break; } } /* TRUE for match, FALSE otherwise */ typedef gboolean (*NiceStreamItemFindFunc) (struct NiceStreamItem * item, gpointer user_data); struct nice_find { NiceStreamItemFindFunc func; gpointer data; struct NiceStreamItem *ret; }; static gboolean _find_nice_item (struct NiceStreamItem *item, gpointer user_data) { struct nice_find *f = user_data; if (f->func (item, f->data)) { f->ret = item; return FALSE; } return TRUE; } static struct NiceStreamItem * _nice_stream_item_find (GstWebRTCNice * ice, NiceStreamItemFindFunc func, gpointer data) { struct nice_find f; f.func = func; f.data = data; f.ret = NULL; _nice_stream_item_foreach (ice, _find_nice_item, &f); return f.ret; } #define NICE_MATCH_INIT { -1, -1, NULL } static gboolean _match (struct NiceStreamItem *item, struct NiceStreamItem *m) { if (m->session_id != -1 && m->session_id != item->session_id) return FALSE; if (m->nice_stream_id != -1 && m->nice_stream_id != item->nice_stream_id) return FALSE; if (m->stream != NULL && m->stream != item->stream) return FALSE; return TRUE; } static struct NiceStreamItem * _find_item (GstWebRTCNice * ice, guint session_id, guint nice_stream_id, GstWebRTCICEStream * stream) { struct NiceStreamItem m = NICE_MATCH_INIT; m.session_id = session_id; m.nice_stream_id = nice_stream_id; m.stream = stream; return _nice_stream_item_find (ice, (NiceStreamItemFindFunc) _match, &m); } static struct NiceStreamItem * _create_nice_stream_item (GstWebRTCNice * ice, guint session_id) { struct NiceStreamItem item; item.session_id = session_id; item.nice_stream_id = nice_agent_add_stream (ice->priv->nice_agent, 1); item.stream = GST_WEBRTC_ICE_STREAM (gst_webrtc_nice_stream_new (GST_WEBRTC_ICE (ice), item.nice_stream_id) ); g_array_append_val (ice->priv->nice_stream_map, item); return _find_item (ice, item.session_id, item.nice_stream_id, item.stream); } static void _parse_userinfo (const gchar * userinfo, gchar ** user, gchar ** pass) { const gchar *colon; if (!userinfo) { *user = NULL; *pass = NULL; return; } colon = g_strstr_len (userinfo, -1, ":"); if (!colon) { *user = g_uri_unescape_string (userinfo, NULL); *pass = NULL; return; } /* Check that the first occurence is also the last occurence */ if (colon != g_strrstr (userinfo, ":")) GST_WARNING ("userinfo %s contains more than one ':', will assume that the " "first ':' delineates user:pass. You should escape the user and pass " "before adding to the URI.", userinfo); *user = g_uri_unescape_segment (userinfo, colon, NULL); *pass = g_uri_unescape_string (&colon[1], NULL); } struct resolve_host_data { GstWebRTCNice *ice; char *host; gboolean main_context_handled; gpointer user_data; GDestroyNotify notify; }; static void on_resolve_host (GResolver * resolver, GAsyncResult * res, gpointer user_data) { GTask *task = user_data; struct resolve_host_data *rh; GError *error = NULL; GList *addresses; rh = g_task_get_task_data (task); if (!(addresses = g_resolver_lookup_by_name_finish (resolver, res, &error))) { GST_ERROR ("failed to resolve: %s", error->message); g_task_return_error (task, error); g_object_unref (task); return; } GST_DEBUG_OBJECT (rh->ice, "Resolved %d addresses for host %s with data %p", g_list_length (addresses), rh->host, rh); g_task_return_pointer (task, addresses, (GDestroyNotify) g_resolver_free_addresses); g_object_unref (task); } static void free_resolve_host_data (struct resolve_host_data *rh) { GST_TRACE_OBJECT (rh->ice, "Freeing data %p for resolving host %s", rh, rh->host); if (rh->notify) rh->notify (rh->user_data); g_free (rh->host); g_free (rh); } static struct resolve_host_data * resolve_host_data_new (GstWebRTCNice * ice, const char *host) { struct resolve_host_data *rh = g_new0 (struct resolve_host_data, 1); rh->ice = ice; rh->host = g_strdup (host); return rh; } static gboolean resolve_host_main_cb (gpointer user_data) { GResolver *resolver = g_resolver_get_default (); GTask *task = user_data; struct resolve_host_data *rh; rh = g_task_get_task_data (task); /* no need to error anymore if the main context disappears and this task is * not run */ rh->main_context_handled = TRUE; GST_DEBUG_OBJECT (rh->ice, "Resolving host %s", rh->host); g_resolver_lookup_by_name_async (resolver, rh->host, NULL, (GAsyncReadyCallback) on_resolve_host, g_object_ref (task)); return G_SOURCE_REMOVE; } static void error_task_if_unhandled (GTask * task) { struct resolve_host_data *rh; rh = g_task_get_task_data (task); if (!rh->main_context_handled) { GST_DEBUG_OBJECT (rh->ice, "host resolve for %s with data %p was never " "executed, main context quit?", rh->host, rh); g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "%s", "Cancelled"); } g_object_unref (task); } static void resolve_host_async (GstWebRTCNice * ice, const gchar * host, GAsyncReadyCallback cb, gpointer user_data, GDestroyNotify notify) { struct resolve_host_data *rh = resolve_host_data_new (ice, host); GTask *task; rh->user_data = user_data; rh->notify = notify; task = g_task_new (rh->ice, NULL, cb, user_data); g_task_set_task_data (task, rh, (GDestroyNotify) free_resolve_host_data); GST_TRACE_OBJECT (rh->ice, "invoking main context for resolving host %s " "with data %p", host, rh); g_main_context_invoke_full (ice->priv->main_context, G_PRIORITY_DEFAULT, resolve_host_main_cb, task, (GDestroyNotify) error_task_if_unhandled); } static GList * resolve_host_finish (GstWebRTCNice * ice, GAsyncResult * res, GError ** error) { g_return_val_if_fail (g_task_is_valid (res, ice), NULL); return g_task_propagate_pointer (G_TASK (res), error); } struct turn_server_data { GstUri *uri; guint nice_stream_id; }; static void turn_server_data_free (struct turn_server_data *data) { gst_uri_unref (data->uri); g_free (data); } static void on_turn_server_resolved (GstWebRTCICE * ice, GAsyncResult * res, struct turn_server_data *user_data) { GList *addresses; GError *error = NULL; GstUri *turn_server = user_data->uri; gboolean ret; gchar *user, *pass; const gchar *userinfo, *transport, *scheme; NiceRelayType relays[4] = { 0, }; int i, relay_n = 0; gchar *ip = NULL; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (!(addresses = resolve_host_finish (nice, res, &error))) { GST_WARNING_OBJECT (ice, "failed to resolve turn address: %s", error->message); g_clear_error (&error); return; } /* XXX: only the first IP is used */ ip = g_inet_address_to_string (addresses->data); /* Set the resolved IP as the host since that's what libnice wants */ gst_uri_set_host (turn_server, ip); scheme = gst_uri_get_scheme (turn_server); transport = gst_uri_get_query_value (turn_server, "transport"); userinfo = gst_uri_get_userinfo (turn_server); _parse_userinfo (userinfo, &user, &pass); if (g_strcmp0 (scheme, "turns") == 0) { relays[relay_n++] = NICE_RELAY_TYPE_TURN_TLS; } else if (g_strcmp0 (scheme, "turn") == 0) { if (!transport || g_strcmp0 (transport, "udp") == 0) relays[relay_n++] = NICE_RELAY_TYPE_TURN_UDP; if (!transport || g_strcmp0 (transport, "tcp") == 0) relays[relay_n++] = NICE_RELAY_TYPE_TURN_TCP; } g_assert (relay_n < G_N_ELEMENTS (relays)); for (i = 0; i < relay_n; i++) { ret = nice_agent_set_relay_info (nice->priv->nice_agent, user_data->nice_stream_id, NICE_COMPONENT_TYPE_RTP, gst_uri_get_host (turn_server), gst_uri_get_port (turn_server), user, pass, relays[i]); if (!ret) { gchar *uri = gst_uri_to_string (turn_server); GST_ERROR_OBJECT (ice, "Failed to set TURN server '%s'", uri); g_free (uri); break; } } g_free (user); g_free (pass); g_free (ip); } static void _add_turn_server (GstWebRTCNice * ice, struct NiceStreamItem *item, GstUri * turn_server) { struct turn_server_data *data; const gchar *host; host = gst_uri_get_host (turn_server); if (!host) { GST_ERROR_OBJECT (ice, "Turn server has no host"); return; } data = g_new0 (struct turn_server_data, 1); data->nice_stream_id = item->nice_stream_id; data->uri = gst_uri_copy (turn_server); resolve_host_async (ice, host, (GAsyncReadyCallback) on_turn_server_resolved, data, (GDestroyNotify) turn_server_data_free); } typedef struct { GstWebRTCNice *ice; struct NiceStreamItem *item; } AddTurnServerData; static void _add_turn_server_func (const gchar * uri, GstUri * turn_server, AddTurnServerData * data) { _add_turn_server (data->ice, data->item, turn_server); } static void on_stun_server_resolved (GstWebRTCNice * ice, GAsyncResult * res, gpointer user_data) { GList *addresses; GError *error = NULL; guint port = GPOINTER_TO_UINT (user_data); char *ip; if (!(addresses = resolve_host_finish (ice, res, &error))) { GST_WARNING_OBJECT (ice, "Failed to resolve stun server: %s", error->message); g_clear_error (&error); return; } /* XXX: only the first IP is used */ ip = g_inet_address_to_string (addresses->data); g_object_set (ice->priv->nice_agent, "stun-server", ip, "stun-server-port", port, NULL); g_free (ip); } static void _add_stun_server (GstWebRTCNice * ice, GstUri * stun_server) { const gchar *msg = "must be of the form stun://:"; const gchar *host; gchar *s = NULL; guint port; s = gst_uri_to_string (stun_server); GST_DEBUG_OBJECT (ice, "adding stun server, %s", s); host = gst_uri_get_host (stun_server); if (!host) { GST_ERROR_OBJECT (ice, "Stun server '%s' has no host, %s", s, msg); goto out; } port = gst_uri_get_port (stun_server); if (port == GST_URI_NO_PORT) { GST_INFO_OBJECT (ice, "Stun server '%s' has no port, assuming 3478", s); port = 3478; gst_uri_set_port (stun_server, port); } resolve_host_async (ice, host, (GAsyncReadyCallback) on_stun_server_resolved, GUINT_TO_POINTER (port), NULL); out: g_free (s); } static GstWebRTCICEStream * gst_webrtc_nice_add_stream (GstWebRTCICE * ice, guint session_id) { struct NiceStreamItem m = NICE_MATCH_INIT; struct NiceStreamItem *item; AddTurnServerData add_data; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); m.session_id = session_id; item = _nice_stream_item_find (nice, (NiceStreamItemFindFunc) _match, &m); if (item) { GST_ERROR_OBJECT (nice, "stream already added with session_id=%u", session_id); return 0; } if (nice->priv->stun_server) { _add_stun_server (nice, nice->priv->stun_server); } item = _create_nice_stream_item (nice, session_id); if (nice->priv->turn_server) { _add_turn_server (nice, item, nice->priv->turn_server); } add_data.ice = nice; add_data.item = item; g_hash_table_foreach (nice->priv->turn_servers, (GHFunc) _add_turn_server_func, &add_data); return item->stream; } static void _on_new_candidate (NiceAgent * agent, NiceCandidate * candidate, GstWebRTCNice * ice) { struct NiceStreamItem *item; gchar *attr; item = _find_item (ice, -1, candidate->stream_id, NULL); if (!item) { GST_WARNING_OBJECT (ice, "received signal for non-existent stream %u", candidate->stream_id); return; } if (!candidate->username || !candidate->password) { gboolean got_credentials; gchar *ufrag, *password; got_credentials = nice_agent_get_local_credentials (ice->priv->nice_agent, candidate->stream_id, &ufrag, &password); g_warn_if_fail (got_credentials); if (!candidate->username) candidate->username = ufrag; else g_free (ufrag); if (!candidate->password) candidate->password = password; else g_free (password); } attr = nice_agent_generate_local_candidate_sdp (agent, candidate); if (ice->priv->on_candidate) ice->priv->on_candidate (GST_WEBRTC_ICE (ice), item->session_id, attr, ice->priv->on_candidate_data); g_free (attr); } static GstWebRTCICETransport * gst_webrtc_nice_find_transport (GstWebRTCICE * ice, GstWebRTCICEStream * stream, GstWebRTCICEComponent component) { struct NiceStreamItem *item; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); item = _find_item (nice, -1, -1, stream); g_return_val_if_fail (item != NULL, NULL); return gst_webrtc_ice_stream_find_transport (item->stream, component); } #if 0 /* TODO don't rely on libnice to (de)serialize candidates */ static NiceCandidateType _candidate_type_from_string (const gchar * s) { if (g_strcmp0 (s, "host") == 0) { return NICE_CANDIDATE_TYPE_HOST; } else if (g_strcmp0 (s, "srflx") == 0) { return NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE; } else if (g_strcmp0 (s, "prflx") == 0) { /* FIXME: is the right string? */ return NICE_CANDIDATE_TYPE_PEER_REFLEXIVE; } else if (g_strcmp0 (s, "relay") == 0) { return NICE_CANDIDATE_TYPE_RELAY; } else { g_assert_not_reached (); return 0; } } static const gchar * _candidate_type_to_string (NiceCandidateType type) { switch (type) { case NICE_CANDIDATE_TYPE_HOST: return "host"; case NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE: return "srflx"; case NICE_CANDIDATE_TYPE_PEER_REFLEXIVE: return "prflx"; case NICE_CANDIDATE_TYPE_RELAY: return "relay"; default: g_assert_not_reached (); return NULL; } } static NiceCandidateTransport _candidate_transport_from_string (const gchar * s) { if (g_strcmp0 (s, "UDP") == 0) { return NICE_CANDIDATE_TRANSPORT_UDP; } else if (g_strcmp0 (s, "TCP tcptype") == 0) { return NICE_CANDIDATE_TRANSPORT_TCP_ACTIVE; } else if (g_strcmp0 (s, "tcp-passive") == 0) { /* FIXME: is the right string? */ return NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE; } else if (g_strcmp0 (s, "tcp-so") == 0) { return NICE_CANDIDATE_TRANSPORT_TCP_SO; } else { g_assert_not_reached (); return 0; } } static const gchar * _candidate_type_to_string (NiceCandidateType type) { switch (type) { case NICE_CANDIDATE_TYPE_HOST: return "host"; case NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE: return "srflx"; case NICE_CANDIDATE_TYPE_PEER_REFLEXIVE: return "prflx"; case NICE_CANDIDATE_TYPE_RELAY: return "relay"; default: g_assert_not_reached (); return NULL; } } #endif /* parse the address for possible resolution */ static gboolean get_candidate_address (const gchar * candidate, gchar ** prefix, gchar ** address, gchar ** postfix) { char **tokens = NULL; if (!g_str_has_prefix (candidate, "a=candidate:")) { GST_ERROR ("candidate \"%s\" does not start with \"a=candidate:\"", candidate); goto failure; } if (!(tokens = g_strsplit (candidate, " ", 6))) { GST_ERROR ("candidate \"%s\" could not be tokenized", candidate); goto failure; } if (g_strv_length (tokens) < 6) { GST_ERROR ("candidate \"%s\" tokenization resulted in not enough tokens", candidate); goto failure; } if (address) *address = g_strdup (tokens[4]); tokens[4] = NULL; if (prefix) *prefix = g_strjoinv (" ", tokens); if (postfix) *postfix = g_strdup (tokens[5]); g_strfreev (tokens); return TRUE; failure: if (tokens) g_strfreev (tokens); return FALSE; } struct resolve_candidate_data { guint nice_stream_id; char *prefix; char *postfix; }; static void free_resolve_candidate_data (struct resolve_candidate_data *rc) { g_free (rc->prefix); g_free (rc->postfix); g_free (rc); } static void add_ice_candidate_to_libnice (GstWebRTCICE * ice, guint nice_stream_id, NiceCandidate * cand) { GSList *candidates = NULL; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (cand->component_id == 2) { /* we only support rtcp-mux so rtcp candidates are useless for us */ GST_INFO_OBJECT (ice, "Dropping RTCP candidate"); return; } candidates = g_slist_append (candidates, cand); nice_agent_set_remote_candidates (nice->priv->nice_agent, nice_stream_id, cand->component_id, candidates); g_slist_free (candidates); } static void on_candidate_resolved (GstWebRTCICE * ice, GAsyncResult * res, gpointer user_data) { struct resolve_candidate_data *rc = user_data; GError *error = NULL; GList *addresses; char *new_candv[4] = { NULL, }; char *new_addr, *new_candidate; NiceCandidate *cand; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (!(addresses = resolve_host_finish (nice, res, &error))) { GST_WARNING_OBJECT (ice, "Could not resolve candidate address: %s", error->message); g_clear_error (&error); return; } new_addr = g_inet_address_to_string (addresses->data); new_candv[0] = rc->prefix; new_candv[1] = new_addr; new_candv[2] = rc->postfix; new_candv[3] = NULL; new_candidate = g_strjoinv (" ", new_candv); GST_DEBUG_OBJECT (ice, "resolved to candidate %s", new_candidate); cand = nice_agent_parse_remote_candidate_sdp (nice->priv->nice_agent, rc->nice_stream_id, new_candidate); g_free (new_candidate); if (!cand) { GST_WARNING_OBJECT (ice, "Could not parse candidate \'%s\'", new_candidate); return; } g_free (new_addr); add_ice_candidate_to_libnice (ice, rc->nice_stream_id, cand); nice_candidate_free (cand); } /* candidate must start with "a=candidate:" or be NULL*/ static void gst_webrtc_nice_add_candidate (GstWebRTCICE * ice, GstWebRTCICEStream * stream, const gchar * candidate) { struct NiceStreamItem *item; NiceCandidate *cand; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); item = _find_item (nice, -1, -1, stream); g_return_if_fail (item != NULL); if (candidate == NULL) { nice_agent_peer_candidate_gathering_done (nice->priv->nice_agent, item->nice_stream_id); return; } cand = nice_agent_parse_remote_candidate_sdp (nice->priv->nice_agent, item->nice_stream_id, candidate); if (!cand) { /* might be a .local candidate */ char *prefix = NULL, *address = NULL, *postfix = NULL; struct resolve_candidate_data *rc; if (!get_candidate_address (candidate, &prefix, &address, &postfix)) { GST_WARNING_OBJECT (nice, "Failed to retrieve address from candidate %s", candidate); goto done; } if (!g_str_has_suffix (address, ".local")) { GST_WARNING_OBJECT (nice, "candidate address \'%s\' does not end " "with \'.local\'", address); goto done; } rc = g_new0 (struct resolve_candidate_data, 1); rc->nice_stream_id = item->nice_stream_id; rc->prefix = prefix; rc->postfix = postfix; resolve_host_async (nice, address, (GAsyncReadyCallback) on_candidate_resolved, rc, (GDestroyNotify) free_resolve_candidate_data); prefix = NULL; postfix = NULL; done: g_clear_pointer (&address, g_free); g_clear_pointer (&prefix, g_free); g_clear_pointer (&postfix, g_free); return; } add_ice_candidate_to_libnice (ice, item->nice_stream_id, cand); nice_candidate_free (cand); } static gboolean gst_webrtc_nice_set_remote_credentials (GstWebRTCICE * ice, GstWebRTCICEStream * stream, gchar * ufrag, gchar * pwd) { struct NiceStreamItem *item; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); g_return_val_if_fail (ufrag != NULL, FALSE); g_return_val_if_fail (pwd != NULL, FALSE); item = _find_item (nice, -1, -1, stream); g_return_val_if_fail (item != NULL, FALSE); GST_DEBUG_OBJECT (nice, "Setting remote ICE credentials on " "ICE stream %u ufrag:%s pwd:%s", item->nice_stream_id, ufrag, pwd); nice_agent_set_remote_credentials (nice->priv->nice_agent, item->nice_stream_id, ufrag, pwd); return TRUE; } static gboolean gst_webrtc_nice_add_turn_server (GstWebRTCICE * ice, const gchar * uri) { gboolean ret = FALSE; GstUri *valid_uri; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (!(valid_uri = _validate_turn_server (nice, uri))) goto done; g_hash_table_insert (nice->priv->turn_servers, g_strdup (uri), valid_uri); ret = TRUE; done: return ret; } static gboolean gst_webrtc_nice_add_local_ip_address (GstWebRTCNice * ice, const gchar * address) { gboolean ret = FALSE; NiceAddress nice_addr; nice_address_init (&nice_addr); ret = nice_address_set_from_string (&nice_addr, address); if (ret) { ret = nice_agent_add_local_address (ice->priv->nice_agent, &nice_addr); if (!ret) { GST_ERROR_OBJECT (ice, "Failed to add local address to NiceAgent"); } } else { GST_ERROR_OBJECT (ice, "Failed to initialize NiceAddress [%s]", address); } return ret; } static gboolean gst_webrtc_nice_set_local_credentials (GstWebRTCICE * ice, GstWebRTCICEStream * stream, gchar * ufrag, gchar * pwd) { struct NiceStreamItem *item; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); g_return_val_if_fail (ufrag != NULL, FALSE); g_return_val_if_fail (pwd != NULL, FALSE); item = _find_item (nice, -1, -1, stream); g_return_val_if_fail (item != NULL, FALSE); GST_DEBUG_OBJECT (nice, "Setting local ICE credentials on " "ICE stream %u ufrag:%s pwd:%s", item->nice_stream_id, ufrag, pwd); nice_agent_set_local_credentials (nice->priv->nice_agent, item->nice_stream_id, ufrag, pwd); return TRUE; } static gboolean gst_webrtc_nice_gather_candidates (GstWebRTCICE * ice, GstWebRTCICEStream * stream) { struct NiceStreamItem *item; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); item = _find_item (nice, -1, -1, stream); g_return_val_if_fail (item != NULL, FALSE); GST_DEBUG_OBJECT (nice, "gather candidates for stream %u", item->nice_stream_id); return gst_webrtc_ice_stream_gather_candidates (stream); } static void gst_webrtc_nice_set_is_controller (GstWebRTCICE * ice, gboolean controller) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); g_object_set (G_OBJECT (nice->priv->nice_agent), "controlling-mode", controller, NULL); } static gboolean gst_webrtc_nice_get_is_controller (GstWebRTCICE * ice) { gboolean ret; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); g_object_get (G_OBJECT (nice->priv->nice_agent), "controlling-mode", &ret, NULL); return ret; } static void gst_webrtc_nice_set_force_relay (GstWebRTCICE * ice, gboolean force_relay) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); g_object_set (G_OBJECT (nice->priv->nice_agent), "force-relay", force_relay, NULL); } static void gst_webrtc_nice_set_on_ice_candidate (GstWebRTCICE * ice, GstWebRTCICEOnCandidateFunc func, gpointer user_data, GDestroyNotify notify) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (nice->priv->on_candidate_notify) nice->priv->on_candidate_notify (nice->priv->on_candidate_data); nice->priv->on_candidate = NULL; nice->priv->on_candidate = func; nice->priv->on_candidate_data = user_data; nice->priv->on_candidate_notify = notify; } static void gst_webrtc_nice_set_tos (GstWebRTCICE * ice, GstWebRTCICEStream * stream, guint tos) { struct NiceStreamItem *item; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); item = _find_item (nice, -1, -1, stream); g_return_if_fail (item != NULL); nice_agent_set_stream_tos (nice->priv->nice_agent, item->nice_stream_id, tos); } static const gchar * _relay_type_to_string (GstUri * turn_server) { const gchar *scheme; const gchar *transport; if (!turn_server) return "none"; scheme = gst_uri_get_scheme (turn_server); transport = gst_uri_get_query_value (turn_server, "transport"); if (g_strcmp0 (scheme, "turns") == 0) { return "tls"; } else if (g_strcmp0 (scheme, "turn") == 0) { if (!transport || g_strcmp0 (transport, "udp") == 0) return "udp"; if (!transport || g_strcmp0 (transport, "tcp") == 0) return "tcp"; } return "none"; } static gchar * _get_server_url (GstWebRTCNice * ice, NiceCandidate * cand) { switch (cand->type) { case NICE_CANDIDATE_TYPE_RELAYED:{ #if NICE_CHECK_VERSION(0, 1, 19) NiceAddress addr; gchar ipaddr[NICE_ADDRESS_STRING_LEN]; nice_candidate_relay_address (cand, &addr); nice_address_to_string (&addr, ipaddr); return g_strdup (ipaddr); #else static gboolean warned = FALSE; if (!warned) { GST_WARNING ("libnice version < 0.1.19 detected, relayed candidate server address might be wrong."); warned = TRUE; } return g_strdup (gst_uri_get_host (ice->priv->turn_server)); #endif } case NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:{ #if NICE_CHECK_VERSION(0, 1, 20) NiceAddress addr; gchar ipaddr[NICE_ADDRESS_STRING_LEN]; if (nice_candidate_stun_server_address (cand, &addr)) { nice_address_to_string (&addr, ipaddr); return g_strdup (ipaddr); } else { return g_strdup (gst_uri_get_host (ice->priv->stun_server)); } #else static gboolean warned = FALSE; if (!warned) { GST_WARNING ("libnice version < 0.1.20 detected, server-reflexive candidate server " "address might be wrong."); warned = TRUE; } #endif return g_strdup (gst_uri_get_host (ice->priv->stun_server)); } default: return g_strdup (""); } } /* TODO: replace it with nice_candidate_type_to_string() * when it's ready for use * https://libnice.freedesktop.org/libnice/NiceCandidate.html#nice-candidate-type-to-string */ static const gchar * _candidate_type_to_string (NiceCandidateType type) { switch (type) { case NICE_CANDIDATE_TYPE_HOST: return "host"; case NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE: return "srflx"; case NICE_CANDIDATE_TYPE_PEER_REFLEXIVE: return "prflx"; case NICE_CANDIDATE_TYPE_RELAYED: return "relay"; default: g_assert_not_reached (); return NULL; } } static void _populate_candidate_stats (GstWebRTCNice * ice, NiceCandidate * cand, GstWebRTCICEStream * stream, GstWebRTCICECandidateStats * stats, gboolean is_local) { gchar ipaddr[INET6_ADDRSTRLEN]; g_assert (cand != NULL); nice_address_to_string (&cand->addr, ipaddr); stats->port = nice_address_get_port (&cand->addr); stats->ipaddr = g_strdup (ipaddr); stats->stream_id = stream->stream_id; stats->type = _candidate_type_to_string (cand->type); stats->prio = cand->priority; stats->proto = cand->transport == NICE_CANDIDATE_TRANSPORT_UDP ? "udp" : "tcp"; if (is_local) { if (cand->type == NICE_CANDIDATE_TYPE_RELAYED) stats->relay_proto = _relay_type_to_string (ice->priv->turn_server); stats->url = _get_server_url (ice, cand); } } static void _populate_candidate_list_stats (GstWebRTCNice * ice, GSList * cands, GstWebRTCICEStream * stream, GArray * result, gboolean is_local) { GSList *item; for (item = cands; item != NULL; item = item->next) { GstWebRTCICECandidateStats stats; NiceCandidate *c = item->data; _populate_candidate_stats (ice, c, stream, &stats, is_local); g_array_append_val (result, stats); } } static GstWebRTCICECandidateStats * gst_webrtc_nice_get_local_candidates (GstWebRTCICE * ice, GstWebRTCICEStream * stream) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); GSList *cands = NULL; GArray *result = g_array_new (TRUE, TRUE, sizeof (GstWebRTCICECandidateStats)); cands = nice_agent_get_local_candidates (nice->priv->nice_agent, stream->stream_id, NICE_COMPONENT_TYPE_RTP); _populate_candidate_list_stats (nice, cands, stream, result, TRUE); g_slist_free_full (cands, (GDestroyNotify) nice_candidate_free); return (GstWebRTCICECandidateStats *) g_array_free (result, FALSE); } static GstWebRTCICECandidateStats * gst_webrtc_nice_get_remote_candidates (GstWebRTCICE * ice, GstWebRTCICEStream * stream) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); GSList *cands = NULL; GArray *result = g_array_new (TRUE, TRUE, sizeof (GstWebRTCICECandidateStats)); cands = nice_agent_get_remote_candidates (nice->priv->nice_agent, stream->stream_id, NICE_COMPONENT_TYPE_RTP); _populate_candidate_list_stats (nice, cands, stream, result, FALSE); g_slist_free_full (cands, (GDestroyNotify) nice_candidate_free); return (GstWebRTCICECandidateStats *) g_array_free (result, FALSE); } static gboolean gst_webrtc_nice_get_selected_pair (GstWebRTCICE * ice, GstWebRTCICEStream * stream, GstWebRTCICECandidateStats ** local_stats, GstWebRTCICECandidateStats ** remote_stats) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); NiceCandidate *local_cand = NULL; NiceCandidate *remote_cand = NULL; if (stream) { if (nice_agent_get_selected_pair (nice->priv->nice_agent, stream->stream_id, NICE_COMPONENT_TYPE_RTP, &local_cand, &remote_cand)) { *local_stats = g_new0 (GstWebRTCICECandidateStats, 1); _populate_candidate_stats (nice, local_cand, stream, *local_stats, TRUE); *remote_stats = g_new0 (GstWebRTCICECandidateStats, 1); _populate_candidate_stats (nice, remote_cand, stream, *remote_stats, FALSE); return TRUE; } } return FALSE; } static void _clear_ice_stream (struct NiceStreamItem *item) { GstWebRTCNice *ice = NULL; if (!item) return; if (item->stream) { g_object_get (item->stream, "ice", &ice, NULL); if (ice != NULL) { g_signal_handlers_disconnect_by_data (ice->priv->nice_agent, item->stream); gst_object_unref (ice); } gst_object_unref (item->stream); } } static GstUri * _validate_turn_server (GstWebRTCNice * ice, const gchar * s) { GstUri *uri = gst_uri_from_string_escaped (s); const gchar *userinfo, *scheme; GList *keys = NULL, *l; gchar *user = NULL, *pass = NULL; gboolean turn_tls = FALSE; guint port; GST_DEBUG_OBJECT (ice, "validating turn server, %s", s); if (!uri) { GST_ERROR_OBJECT (ice, "Could not parse turn server '%s'", s); return NULL; } scheme = gst_uri_get_scheme (uri); if (g_strcmp0 (scheme, "turn") == 0) { } else if (g_strcmp0 (scheme, "turns") == 0) { turn_tls = TRUE; } else { GST_ERROR_OBJECT (ice, "unknown scheme '%s'", scheme); goto out; } keys = gst_uri_get_query_keys (uri); for (l = keys; l; l = l->next) { gchar *key = l->data; if (g_strcmp0 (key, "transport") == 0) { const gchar *transport = gst_uri_get_query_value (uri, "transport"); if (!transport) { } else if (g_strcmp0 (transport, "udp") == 0) { } else if (g_strcmp0 (transport, "tcp") == 0) { } else { GST_ERROR_OBJECT (ice, "unknown transport value, '%s'", transport); goto out; } } else { GST_ERROR_OBJECT (ice, "unknown query key, '%s'", key); goto out; } } /* TODO: Implement error checking similar to the stun server below */ userinfo = gst_uri_get_userinfo (uri); _parse_userinfo (userinfo, &user, &pass); if (!user) { GST_ERROR_OBJECT (ice, "No username specified in '%s'", s); goto out; } if (!pass) { GST_ERROR_OBJECT (ice, "No password specified in '%s'", s); goto out; } port = gst_uri_get_port (uri); if (port == GST_URI_NO_PORT) { if (turn_tls) { gst_uri_set_port (uri, 5349); } else { gst_uri_set_port (uri, 3478); } } out: g_list_free (keys); g_free (user); g_free (pass); return uri; } static void gst_webrtc_nice_set_stun_server (GstWebRTCICE * ice, const gchar * uri_s) { GstUri *uri = gst_uri_from_string_escaped (uri_s); const gchar *msg = "must be of the form stun://:"; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); GST_DEBUG_OBJECT (nice, "setting stun server, %s", uri_s); if (!uri) { GST_ERROR_OBJECT (nice, "Couldn't parse stun server '%s', %s", uri_s, msg); return; } if (nice->priv->stun_server) gst_uri_unref (nice->priv->stun_server); nice->priv->stun_server = uri; } static gchar * gst_webrtc_nice_get_stun_server (GstWebRTCICE * ice) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (nice->priv->stun_server) return gst_uri_to_string (nice->priv->stun_server); else return NULL; } static void gst_webrtc_nice_set_turn_server (GstWebRTCICE * ice, const gchar * uri_s) { GstUri *uri; GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); uri = _validate_turn_server (nice, uri_s); if (uri) { if (nice->priv->turn_server) gst_uri_unref (nice->priv->turn_server); nice->priv->turn_server = uri; } } static gchar * gst_webrtc_nice_get_turn_server (GstWebRTCICE * ice) { GstWebRTCNice *nice = GST_WEBRTC_NICE (ice); if (nice->priv->turn_server) return gst_uri_to_string (nice->priv->turn_server); else return NULL; } static void gst_webrtc_nice_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstWebRTCICE *ice = GST_WEBRTC_ICE (object); GstWebRTCNice *nice = GST_WEBRTC_NICE (object); switch (prop_id) { case PROP_ICE_TCP: g_object_set_property (G_OBJECT (nice->priv->nice_agent), "ice-tcp", value); break; case PROP_ICE_UDP: g_object_set_property (G_OBJECT (nice->priv->nice_agent), "ice-udp", value); break; case PROP_MIN_RTP_PORT: ice->min_rtp_port = g_value_get_uint (value); if (ice->min_rtp_port > ice->max_rtp_port) g_warning ("Set min-rtp-port to %u which is larger than" " max-rtp-port %u", ice->min_rtp_port, ice->max_rtp_port); break; case PROP_MAX_RTP_PORT: ice->max_rtp_port = g_value_get_uint (value); if (ice->min_rtp_port > ice->max_rtp_port) g_warning ("Set max-rtp-port to %u which is smaller than" " min-rtp-port %u", ice->max_rtp_port, ice->min_rtp_port); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_webrtc_nice_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstWebRTCICE *ice = GST_WEBRTC_ICE (object); GstWebRTCNice *nice = GST_WEBRTC_NICE (object); switch (prop_id) { case PROP_AGENT: g_value_set_object (value, nice->priv->nice_agent); break; case PROP_ICE_TCP: g_object_get_property (G_OBJECT (nice->priv->nice_agent), "ice-tcp", value); break; case PROP_ICE_UDP: g_object_get_property (G_OBJECT (nice->priv->nice_agent), "ice-udp", value); break; case PROP_MIN_RTP_PORT: g_value_set_uint (value, ice->min_rtp_port); break; case PROP_MAX_RTP_PORT: g_value_set_uint (value, ice->max_rtp_port); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_webrtc_nice_finalize (GObject * object) { GstWebRTCNice *ice = GST_WEBRTC_NICE (object); g_signal_handlers_disconnect_by_data (ice->priv->nice_agent, ice); _stop_thread (ice); if (ice->priv->on_candidate_notify) ice->priv->on_candidate_notify (ice->priv->on_candidate_data); ice->priv->on_candidate = NULL; ice->priv->on_candidate_notify = NULL; if (ice->priv->turn_server) gst_uri_unref (ice->priv->turn_server); if (ice->priv->stun_server) gst_uri_unref (ice->priv->stun_server); g_mutex_clear (&ice->priv->lock); g_cond_clear (&ice->priv->cond); g_array_free (ice->priv->nice_stream_map, TRUE); g_object_unref (ice->priv->nice_agent); g_hash_table_unref (ice->priv->turn_servers); G_OBJECT_CLASS (parent_class)->finalize (object); } static void gst_webrtc_nice_constructed (GObject * object) { GstWebRTCNice *ice = GST_WEBRTC_NICE (object); NiceAgentOption options = 0; _start_thread (ice); options |= NICE_AGENT_OPTION_ICE_TRICKLE; options |= NICE_AGENT_OPTION_REGULAR_NOMINATION; ice->priv->nice_agent = nice_agent_new_full (ice->priv->main_context, NICE_COMPATIBILITY_RFC5245, options); g_signal_connect (ice->priv->nice_agent, "new-candidate-full", G_CALLBACK (_on_new_candidate), ice); G_OBJECT_CLASS (parent_class)->constructed (object); } static void gst_webrtc_nice_class_init (GstWebRTCNiceClass * klass) { GstWebRTCICEClass *gst_webrtc_ice_class = GST_WEBRTC_ICE_CLASS (klass); GObjectClass *gobject_class = (GObjectClass *) klass; // override virtual functions gst_webrtc_ice_class->add_candidate = gst_webrtc_nice_add_candidate; gst_webrtc_ice_class->add_stream = gst_webrtc_nice_add_stream; gst_webrtc_ice_class->add_turn_server = gst_webrtc_nice_add_turn_server; gst_webrtc_ice_class->find_transport = gst_webrtc_nice_find_transport; gst_webrtc_ice_class->gather_candidates = gst_webrtc_nice_gather_candidates; gst_webrtc_ice_class->get_is_controller = gst_webrtc_nice_get_is_controller; gst_webrtc_ice_class->get_stun_server = gst_webrtc_nice_get_stun_server; gst_webrtc_ice_class->get_turn_server = gst_webrtc_nice_get_turn_server; gst_webrtc_ice_class->set_force_relay = gst_webrtc_nice_set_force_relay; gst_webrtc_ice_class->set_is_controller = gst_webrtc_nice_set_is_controller; gst_webrtc_ice_class->set_local_credentials = gst_webrtc_nice_set_local_credentials; gst_webrtc_ice_class->set_remote_credentials = gst_webrtc_nice_set_remote_credentials; gst_webrtc_ice_class->set_stun_server = gst_webrtc_nice_set_stun_server; gst_webrtc_ice_class->set_tos = gst_webrtc_nice_set_tos; gst_webrtc_ice_class->set_turn_server = gst_webrtc_nice_set_turn_server; gst_webrtc_ice_class->set_on_ice_candidate = gst_webrtc_nice_set_on_ice_candidate; gst_webrtc_ice_class->get_local_candidates = gst_webrtc_nice_get_local_candidates; gst_webrtc_ice_class->get_remote_candidates = gst_webrtc_nice_get_remote_candidates; gst_webrtc_ice_class->get_selected_pair = gst_webrtc_nice_get_selected_pair; gobject_class->constructed = gst_webrtc_nice_constructed; gobject_class->get_property = gst_webrtc_nice_get_property; gobject_class->set_property = gst_webrtc_nice_set_property; gobject_class->finalize = gst_webrtc_nice_finalize; g_object_class_install_property (gobject_class, PROP_AGENT, g_param_spec_object ("agent", "ICE agent", "ICE agent in use by this object. WARNING! Accessing this property " "may have disastrous consequences for the operation of webrtcbin. " "Other ICE implementations may not have the same interface.", NICE_TYPE_AGENT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_ICE_TCP, g_param_spec_boolean ("ice-tcp", "ICE TCP", "Whether the agent should use ICE-TCP when gathering candidates", TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_ICE_UDP, g_param_spec_boolean ("ice-udp", "ICE UDP", "Whether the agent should use ICE-UDP when gathering candidates", TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_signal_override_class_handler ("add-local-ip-address", G_TYPE_FROM_CLASS (klass), G_CALLBACK (gst_webrtc_nice_add_local_ip_address)); } static void gst_webrtc_nice_init (GstWebRTCNice * ice) { ice->priv = gst_webrtc_nice_get_instance_private (ice); g_mutex_init (&ice->priv->lock); g_cond_init (&ice->priv->cond); ice->priv->turn_servers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) gst_uri_unref); ice->priv->nice_stream_map = g_array_new (FALSE, TRUE, sizeof (struct NiceStreamItem)); g_array_set_clear_func (ice->priv->nice_stream_map, (GDestroyNotify) _clear_ice_stream); } GstWebRTCNice * gst_webrtc_nice_new (const gchar * name) { return g_object_new (GST_TYPE_WEBRTC_NICE, "name", name, NULL); }