Add API set for ICE candidate 71/243671/10
authorSangchul Lee <sc11.lee@samsung.com>
Wed, 9 Sep 2020 04:56:06 +0000 (13:56 +0900)
committerSangchul Lee <sc11.lee@samsung.com>
Tue, 22 Sep 2020 06:31:23 +0000 (15:31 +0900)
These correspond to the 'onicecandidate' property
and 'addIceCandidate' method of the RTCPeerConnection
respectively.

Functions are added as below.
 - webrtc_set_ice_candidate_cb()
 - webrtc_unset_ice_candidate_cb()
 - webrtc_add_ice_candidate()

Test cases for these functions are added to webrtc_test.
Some release handle information are added to @remarks.

[Version] 0.1.19
[Issue Type] API

Change-Id: Ib6675943d3aa2917360b8de82a4e76700089c961
Signed-off-by: Sangchul Lee <sc11.lee@samsung.com>
include/webrtc.h
include/webrtc_private.h
packaging/capi-media-webrtc.spec
src/webrtc.c
src/webrtc_private.c
test/webrtc_test.c

index cfcaea91048552383a22b96e2519a343385ef475..1af37c058b9147d47316236c99b3e89e1a95da6e 100644 (file)
@@ -83,7 +83,7 @@ typedef enum {
 /**
  * @brief Called when an error occurs.
  * @since_tizen 6.0
- * @remarks The @a webrtc is the same object for which the callback was set.
+ * @remarks The @a webrtc is the same object for which the callback was set.\nThe @a webrtc should not be released.
  * @param [in] webrtc     WebRTC handle
  * @param [in] error      The error code
  * @param [in] state      The current state of the WebRTC handle
@@ -96,7 +96,7 @@ typedef void (*webrtc_error_cb)(webrtc_h webrtc, webrtc_error_e error, webrtc_st
 /**
  * @brief Called when the WebRTC state is changed.
  * @since_tizen 6.0
- * @remarks The @a webrtc is the same object for which the callback was set.
+ * @remarks The @a webrtc is the same object for which the callback was set.\nThe @a webrtc should not be released.
  * @param [in] webrtc     WebRTC handle
  * @param [in] previous   The previous state of the WebRTC handle
  * @param [in] current    The current state of the WebRTC handle
@@ -109,6 +109,7 @@ typedef void (*webrtc_state_changed_cb)(webrtc_h webrtc, webrtc_state_e previous
 /**
  * @brief Called when the WebRTC needs session negotiation.
  * @since_tizen 6.0
+ * @remarks The @a webrtc is the same object for which the callback was set.\nThe @a webrtc should not be released.
  * @param [in] webrtc     WebRTC handle
  * @param [in] user_data  The user data passed from the callback registration function
  * @see webrtc_set_negotiation_needed_cb()
@@ -116,6 +117,18 @@ typedef void (*webrtc_state_changed_cb)(webrtc_h webrtc, webrtc_state_e previous
  */
 typedef void (*webrtc_negotiation_needed_cb)(webrtc_h webrtc, void *user_data);
 
+/**
+ * @brief Called when the WebRTC needs to send the ICE candidate message to the remote peer through the signaling channel.
+ * @since_tizen 6.0
+ * @remarks The @a webrtc is the same object for which the callback was set.\nThe @a webrtc should not be released.
+ * @param [in] webrtc     WebRTC handle
+ * @param [in] candidate  The ICE candidate message
+ * @param [in] user_data  The user data passed from the callback registration function
+ * @see webrtc_set_ice_candidate_cb()
+ * @see webrtc_unset_ice_candidate_cb()
+ */
+typedef void (*webrtc_ice_candidate_cb)(webrtc_h webrtc, const char *candidate, void *user_data);
+
 /**
  * @brief Sets a callback function to be invoked when an asynchronous operation error occurs.
  * @since_tizen 6.0
@@ -331,6 +344,38 @@ int webrtc_set_negotiation_needed_cb(webrtc_h webrtc, webrtc_negotiation_needed_
  */
 int webrtc_unset_negotiation_needed_cb(webrtc_h webrtc);
 
+/**
+ * @brief Sets an ICE candidate callback function to be invoked when the WebRTC needs to send the ICE candidate message to the remote peer.
+ * @since_tizen 6.0
+ * @param [in] webrtc      WebRTC handle
+ * @param [in] callback    Callback function pointer
+ * @param [in] user_data   The user data to be passed to the callback function
+ * @return @c 0 on success,
+ *         otherwise a negative error value
+ * @retval #WEBRTC_ERROR_NONE    Successful
+ * @retval #WEBRTC_ERROR_INVALID_PARAMETER Invalid parameter
+ * @retval #WEBRTC_ERROR_INVALID_OPERATION Invalid operation
+ * @retval #WEBRTC_ERROR_INVALID_STATE Invalid state
+ * @pre @a webrtc state must be set to #WEBRTC_STATE_IDLE.
+ * @post webrtc_ice_candidate_cb() will be invoked.
+ * @see webrtc_unset_ice_candidate_cb()
+ * @see webrtc_ice_candidate_cb()
+ */
+int webrtc_set_ice_candidate_cb(webrtc_h webrtc, webrtc_ice_candidate_cb callback, void *user_data);
+
+/**
+ * @brief Unsets the ice candidate callback function.
+ * @since_tizen 6.0
+ * @param [in] webrtc      WebRTC handle
+ * @return @c 0 on success,
+ *         otherwise a negative error value
+ * @retval #WEBRTC_ERROR_NONE    Successful
+ * @retval #WEBRTC_ERROR_INVALID_PARAMETER Invalid parameter
+ * @retval #WEBRTC_ERROR_INVALID_OPERATION Invalid operation
+ * @see webrtc_set_ice_candidate_cb()
+ */
+int webrtc_unset_ice_candidate_cb(webrtc_h webrtc);
+
 /**
  * @brief Creates SDP offer to start a new WebRTC connection to a remote peer.
  * @since_tizen 6.0
@@ -402,6 +447,23 @@ int webrtc_set_local_description(webrtc_h webrtc, const char *description);
  */
 int webrtc_set_remote_description(webrtc_h webrtc, const char *description);
 
+/**
+ * @brief Adds a new ICE candidate from the remote peer over its signaling channel.
+ * @since_tizen 6.0
+ * @remarks @a candidate is a JSON string.\n
+ *          It will be {"ice":{"candidate":"..."}}.
+ * @param [in] webrtc      WebRTC handle
+ * @param [in] candidate   The ICE candidate
+ * @return @c 0 on success,
+ *         otherwise a negative error value
+ * @retval #WEBRTC_ERROR_NONE    Successful
+ * @retval #WEBRTC_ERROR_INVALID_PARAMETER Invalid parameter
+ * @retval #WEBRTC_ERROR_INVALID_OPERATION Invalid operation
+ * @retval #WEBRTC_ERROR_INVALID_STATE Invalid state
+ * @pre @a webrtc state must be set to #WEBRTC_STATE_PLAYING.
+ */
+int webrtc_add_ice_candidate(webrtc_h webrtc, const char *candidate);
+
 /**
  * @}
  */
index 424c437e0793c6503fb0ecd61a3220bcf5db2baa..02294c99ff3f20b004f055c59f9c379f22f566fb 100644 (file)
@@ -152,6 +152,7 @@ typedef struct _webrtc_s {
        webrtc_callbacks_s error_cb;
        webrtc_callbacks_s state_changed_cb;
        webrtc_callbacks_s negotiation_needed_cb;
+       webrtc_callbacks_s ice_candidate_cb;
 } webrtc_s;
 
 typedef struct _element_info_s {
@@ -176,6 +177,7 @@ int _remove_media_source(webrtc_s *webrtc, unsigned int source_id);
 int _webrtcbin_create_offer(webrtc_s *webrtc, char **offer);
 int _webrtcbin_create_answer(webrtc_s *webrtc, char **answer);
 int _webrtcbin_set_session_description(webrtc_s *webrtc, const char *description, gboolean is_remote);
+int _webrtcbin_add_ice_candidate(webrtc_s *webrtc, const char *candidate);
 
 #ifdef __cplusplus
 }
index 06ebc89ac6f41faf7bfc1b2e8a7e3ffafe05d3e6..9f618303296108e33ccf4c68f2a3721da1f3b193 100644 (file)
@@ -1,6 +1,6 @@
 Name:       capi-media-webrtc
 Summary:    A WebRTC library in Tizen Native API
-Version:    0.1.18
+Version:    0.1.19
 Release:    0
 Group:      Multimedia/API
 License:    Apache-2.0
index f4d86fb81d88d6c3057140254d34e45a3aeae73c..29490e9c71c82d98422735c3c460f15dd9d9b64c 100644 (file)
@@ -326,6 +326,49 @@ int webrtc_unset_negotiation_needed_cb(webrtc_h webrtc)
        return WEBRTC_ERROR_NONE;
 }
 
+int webrtc_set_ice_candidate_cb(webrtc_h webrtc, webrtc_ice_candidate_cb callback, void *user_data)
+{
+       webrtc_s *_webrtc = (webrtc_s*)webrtc;
+
+       RET_VAL_IF(_webrtc == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "webrtc is NULL");
+       RET_VAL_IF(callback == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "callback is NULL");
+
+       g_mutex_lock(&_webrtc->mutex);
+
+       RET_VAL_WITH_UNLOCK_IF(_webrtc->state != WEBRTC_STATE_IDLE, WEBRTC_ERROR_INVALID_STATE, &_webrtc->mutex, "the state should be IDLE");
+
+       _webrtc->ice_candidate_cb.callback = callback;
+       _webrtc->ice_candidate_cb.user_data = user_data;
+
+       LOG_INFO("callback[%p] user_data[%p]", callback, user_data);
+
+       g_mutex_unlock(&_webrtc->mutex);
+
+       return WEBRTC_ERROR_NONE;
+}
+
+int webrtc_unset_ice_candidate_cb(webrtc_h webrtc)
+{
+       webrtc_s *_webrtc = (webrtc_s*)webrtc;
+
+       RET_VAL_IF(_webrtc == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "webrtc is NULL");
+
+       g_mutex_lock(&_webrtc->mutex);
+
+       RET_VAL_WITH_UNLOCK_IF(_webrtc->state != WEBRTC_STATE_IDLE, WEBRTC_ERROR_INVALID_STATE, &_webrtc->mutex, "the state should be IDLE");
+       RET_VAL_WITH_UNLOCK_IF(_webrtc->ice_candidate_cb.callback == NULL, WEBRTC_ERROR_INVALID_OPERATION, &_webrtc->mutex, "callback was not set");
+
+       LOG_INFO("callback[%p] user_data[%p] is reset to NULL",
+               _webrtc->ice_candidate_cb.callback, _webrtc->ice_candidate_cb.user_data);
+
+       _webrtc->ice_candidate_cb.callback = NULL;
+       _webrtc->ice_candidate_cb.user_data = NULL;
+
+       g_mutex_unlock(&_webrtc->mutex);
+
+       return WEBRTC_ERROR_NONE;
+}
+
 int webrtc_create_offer(webrtc_h webrtc, char **offer)
 {
        int ret = WEBRTC_ERROR_NONE;
@@ -409,4 +452,25 @@ int webrtc_set_remote_description(webrtc_h webrtc, const char *description)
        g_mutex_unlock(&_webrtc->mutex);
 
        return ret;
-}
\ No newline at end of file
+}
+
+int webrtc_add_ice_candidate(webrtc_h webrtc, const char *candidate)
+{
+       int ret = WEBRTC_ERROR_NONE;
+       webrtc_s *_webrtc = (webrtc_s*)webrtc;
+
+       RET_VAL_IF(_webrtc == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "webrtc is NULL");
+       RET_VAL_IF(candidate == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "candidate is NULL");
+
+       g_mutex_lock(&_webrtc->mutex);
+
+       RET_VAL_WITH_UNLOCK_IF(_webrtc->state != WEBRTC_STATE_PLAYING, WEBRTC_ERROR_INVALID_STATE, &_webrtc->mutex, "the state should be PLAYING");
+
+       LOG_INFO("candidate[ %s ]", candidate);
+
+       ret = _webrtcbin_add_ice_candidate(webrtc, candidate);
+
+       g_mutex_unlock(&_webrtc->mutex);
+
+       return ret;
+}
index 0ed7b0748fbff57a550af57e8ec964e73431c437..2f6a75e587dec3ece12fd62e997c1d8306b67497 100644 (file)
@@ -62,7 +62,7 @@ do { \
        } \
 } while (0)
 
-/* Use g_free() to free the return value. */
+/* Use g_free() to release the return value. */
 static gchar* __get_string_from_json_object(JsonObject *object)
 {
        JsonNode *root;
@@ -82,6 +82,7 @@ static gchar* __get_string_from_json_object(JsonObject *object)
        return text;
 }
 
+/* Use g_free() to release the return value. */
 static gchar* __make_sdp_message(GstWebRTCSessionDescription *desc)
 {
        gchar *text;
@@ -114,6 +115,31 @@ static gchar* __make_sdp_message(GstWebRTCSessionDescription *desc)
        return text;
 }
 
+/* Use g_free() to release the return value. */
+static gchar *__make_ice_candidate_message(guint mlineindex, gchar *candidate)
+{
+       JsonObject *ice, *msg;
+       gchar *text;
+
+       if (!candidate) {
+               LOG_DEBUG("candidate is NULL, all ICE candidates have been sent");
+               return NULL;
+       }
+
+       ice = json_object_new();
+       json_object_set_string_member(ice, "candidate", candidate);
+       json_object_set_int_member(ice, "sdpMLineIndex", mlineindex);
+
+       msg = json_object_new();
+       json_object_set_object_member(msg, "ice", ice);
+
+       text = __get_string_from_json_object(msg);
+
+       json_object_unref(msg);
+
+       return text;
+}
+
 static gboolean __meet_gst_state(webrtc_state_e state, GstState gst_state)
 {
        if (state == WEBRTC_STATE_IDLE && gst_state == GST_STATE_READY)
@@ -441,6 +467,31 @@ static void __webrtcbin_on_negotiation_needed(GstElement *webrtcbin, gpointer us
        LOG_DEBUG("<<< end of the callback");
 }
 
+static void __webrtcbin_on_ice_candidate(GstElement *webrtcbin, guint mlineindex, gchar *candidate, gpointer user_data)
+{
+       webrtc_s *webrtc = (webrtc_s *)user_data;
+       gchar *_candidate;
+       char *_param_candidate;
+
+       RET_IF(webrtcbin == NULL, "webrtcbin is NULL");
+       RET_IF(webrtc == NULL, "webrtc is NULL");
+       RET_IF(webrtc->ice_candidate_cb.callback == NULL, "ice_candidate_cb is NULL");
+
+       _candidate = __make_ice_candidate_message(mlineindex, candidate);
+       if (!_candidate)
+               return;
+
+       _param_candidate = strdup(_candidate);
+       g_free(_candidate);
+
+       LOG_DEBUG(">>> invoke ice_candidate_cb[%p], user_data[%p]",
+               webrtc->ice_candidate_cb.callback, webrtc->ice_candidate_cb.user_data);
+       ((webrtc_ice_candidate_cb)(webrtc->ice_candidate_cb.callback))((webrtc_h)webrtc, (const char *)_param_candidate, webrtc->ice_candidate_cb.user_data);
+       LOG_DEBUG("<<< end of the callback");
+
+       free(_param_candidate);
+}
+
 int _gst_build_pipeline(webrtc_s *webrtc)
 {
        RET_VAL_IF(webrtc == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "webrtc is NULL");
@@ -463,6 +514,7 @@ int _gst_build_pipeline(webrtc_s *webrtc)
                goto error;
        }
        __connect_and_append_signal(&webrtc->signals, webrtc->gst.webrtcbin, "on-negotiation-needed", G_CALLBACK(__webrtcbin_on_negotiation_needed), webrtc);
+       __connect_and_append_signal(&webrtc->signals, webrtc->gst.webrtcbin, "on-ice-candidate", G_CALLBACK(__webrtcbin_on_ice_candidate), webrtc);
 
        if (!gst_bin_add(GST_BIN(webrtc->gst.pipeline), webrtc->gst.webrtcbin)) {
                LOG_ERROR("failed to gst_bin_add(), [%s] -> [%s] pipeline", GST_ELEMENT_NAME(webrtc->gst.webrtcbin), GST_ELEMENT_NAME(webrtc->gst.pipeline));
@@ -1076,7 +1128,7 @@ int _webrtcbin_create_answer(webrtc_s *webrtc, char **answer)
        return _create_session_description(webrtc, FALSE, answer);
 }
 
-/* Use g_free() to free the sdp and type parameter. */
+/* Use g_free() to release the sdp and type parameter. */
 static int __get_sdp_from_description(const char *description, gchar **sdp, gchar **type)
 {
        int ret = WEBRTC_ERROR_NONE;
@@ -1192,3 +1244,82 @@ end:
        return ret;
 }
 
+/* Use g_free() to release the candidate parameter. */
+static int __get_ice_candidate_from_message(const char *ice_message, gchar **candidate, gint *mlineindex)
+{
+       int ret = WEBRTC_ERROR_NONE;
+       JsonNode *root;
+       JsonObject *object;
+       JsonObject *child;
+       JsonParser *parser;
+       const gchar *_candidate;
+
+       RET_VAL_IF(ice_message == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "ice_message is NULL");
+       RET_VAL_IF(candidate == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "candidate is NULL");
+       RET_VAL_IF(mlineindex == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "mlineindex is NULL");
+
+       parser = json_parser_new();
+       if (!JSON_IS_PARSER(parser))
+               return WEBRTC_ERROR_INVALID_OPERATION;
+
+       if (!json_parser_load_from_data(parser, ice_message, -1, NULL)) {
+               LOG_ERROR("unknown message: %s", ice_message);
+               ret = WEBRTC_ERROR_INVALID_PARAMETER;
+               goto end;
+       }
+
+       root = json_parser_get_root(parser);
+       if (!JSON_NODE_HOLDS_OBJECT(root)) {
+               LOG_ERROR("it does not contain a JsonObject: %s", ice_message);
+               ret = WEBRTC_ERROR_INVALID_PARAMETER;
+               goto end;
+       }
+
+       object = json_node_get_object(root);
+       if (!json_object_has_member(object, "ice")) {
+               LOG_ERROR("It does not contain 'ice' member: %s", ice_message);
+               ret = WEBRTC_ERROR_INVALID_PARAMETER;
+               goto end;
+       }
+
+       child = json_object_get_object_member(object, "ice");
+
+       _candidate = json_object_get_string_member(child, "candidate");
+       if (!_candidate) {
+               LOG_ERROR("Could not find candidate member: %s", ice_message);
+               ret = WEBRTC_ERROR_INVALID_PARAMETER;
+               goto end;
+       }
+
+       *candidate = g_strdup(_candidate);
+       *mlineindex = json_object_get_int_member(child, "sdpMLineIndex");
+
+       LOG_DEBUG("candidate: %s", *candidate);
+       LOG_DEBUG("sdpMLineIndex: %d", *mlineindex);
+end:
+       g_object_unref(parser);
+       return ret;
+}
+
+int _webrtcbin_add_ice_candidate(webrtc_s *webrtc, const char *candidate)
+{
+       int ret = WEBRTC_ERROR_NONE;
+       gchar *_candidate;
+       gint sdpmlineindex;
+
+       RET_VAL_IF(webrtc == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "webrtc is NULL");
+       RET_VAL_IF(candidate == NULL, WEBRTC_ERROR_INVALID_PARAMETER, "candidate is NULL");
+       RET_VAL_IF(webrtc->gst.webrtcbin == NULL, WEBRTC_ERROR_INVALID_OPERATION, "webrtcbin is NULL");
+
+       ret = __get_ice_candidate_from_message(candidate, &_candidate, &sdpmlineindex);
+       if (ret != WEBRTC_ERROR_NONE)
+               return ret;
+
+       g_signal_emit_by_name(webrtc->gst.webrtcbin, "add-ice-candidate", sdpmlineindex, _candidate);
+
+       LOG_DEBUG("[add-ice-candidate] signal is emitted");
+
+       g_free(_candidate);
+
+       return ret;
+}
index eb4995649c62291eaa49c0fe3437724c30ad42dc..ab680e4bd4c073a28f029858f4296815eeaebd36 100644 (file)
@@ -75,6 +75,7 @@ static webrtc_h g_webrtc;
 static char *g_offer;
 static char *g_answer;
 static char *g_remote_desc;
+static GList *g_ice_candidates;
 static SoupWebsocketConnection *g_ws_conn;
 static gint32 g_local_peer_id;
 static gchar g_signaling_server[MAX_STRING_LEN];
@@ -419,6 +420,36 @@ static void _webrtc_unset_negotiation_needed_cb()
                g_print("webrtc_unset_negotiation_needed_cb() success\n");
 }
 
+static void __ice_candidate_cb(webrtc_h webrtc, const char *candidate, void *user_data)
+{
+       g_print("__ice_candidate_cb() is invoked\n");
+
+       g_print("\n[to SERVER > ICE]\n%s\n", candidate);
+       soup_websocket_connection_send_text(g_ws_conn, candidate);
+}
+
+static void _webrtc_set_ice_candidate_cb()
+{
+       int ret = WEBRTC_ERROR_NONE;
+
+       ret = webrtc_set_ice_candidate_cb(g_webrtc, __ice_candidate_cb, g_webrtc);
+       if (ret != WEBRTC_ERROR_NONE)
+               g_print("failed to webrtc_set_ice_candidate_cb()\n");
+       else
+               g_print("webrtc_set_ice_candidate_cb() success\n");
+}
+
+static void _webrtc_unset_ice_candidate_cb()
+{
+       int ret = WEBRTC_ERROR_NONE;
+
+       ret = webrtc_unset_ice_candidate_cb(g_webrtc);
+       if (ret != WEBRTC_ERROR_NONE)
+               g_print("failed to webrtc_unset_ice_candidate_cb()\n");
+       else
+               g_print("webrtc_unset_ice_candidate_cb() success\n");
+}
+
 static void _webrtc_create_offer()
 {
        int ret = WEBRTC_ERROR_NONE;
@@ -473,6 +504,24 @@ static void _webrtc_set_remote_description(void)
                g_print("webrtc_set_remote_description() success\n");
 }
 
+static void __foreach_ice_candidate(gpointer data, gpointer user_data)
+{
+       int ret = WEBRTC_ERROR_NONE;
+
+       ret = webrtc_add_ice_candidate(g_webrtc, (const char *)data);
+       if (ret != WEBRTC_ERROR_NONE)
+               g_print("failed to webrtc_ice_candidate()\n");
+       else
+               g_print("webrtc_ice_candidate() success\n");
+}
+
+static void _webrtc_add_ice_candidate(void)
+{
+       g_list_foreach(g_ice_candidates, __foreach_ice_candidate, NULL);
+
+       g_list_free_full(g_ice_candidates, free);
+}
+
 static void _setting_uri(gchar *dest_arr, char *uri)
 {
        int ret = 0;
@@ -582,7 +631,7 @@ static void __handle_json_structured_message(const gchar *text)
 
        } else if (json_object_has_member(object, "ice")){
                g_print("\n[from SERVER > ICE]\n%s\n", text);
-               /* FIXME: add ice candidates, several candidates will arrive from the server */
+               g_ice_candidates = g_list_append(g_ice_candidates, strdup(text));
 
        } else {
                g_printerr("neither 'sdp' nor 'ice' member exist in JSON message [%s]\n", text);
@@ -708,6 +757,7 @@ void quit_program()
        free(g_offer);
        free(g_answer);
        free(g_remote_desc);
+       g_list_free_full(g_ice_candidates, free);
        elm_exit();
 }
 
@@ -759,6 +809,12 @@ void _interpret_main_menu(char *cmd)
                } else if (strncmp(cmd, "un", 2) == 0) {
                        _webrtc_unset_negotiation_needed_cb();
 
+               } else if (strncmp(cmd, "si", 2) == 0) {
+                       _webrtc_set_ice_candidate_cb();
+
+               } else if (strncmp(cmd, "ui", 2) == 0) {
+                       _webrtc_unset_ice_candidate_cb();
+
                } else if (strncmp(cmd, "co", 2) == 0) {
                        _webrtc_create_offer();
 
@@ -789,6 +845,9 @@ void _interpret_main_menu(char *cmd)
                } else if (strncmp(cmd, "sd", 2) == 0) {
                        g_menu_state = CURRENT_STATUS_SEND_LOCAL_DESCRIPTION;
 
+               } else if (strncmp(cmd, "ac", 2) == 0) {
+                       _webrtc_add_ice_candidate();
+
                } else {
                        g_print("unknown menu \n");
                }
@@ -834,10 +893,13 @@ void display_sub_basic()
        g_print("us. Unset state changed callback\n");
        g_print("sn. Set negotiation needed callback\t");
        g_print("un. Unset negotiation needed callback\n");
+       g_print("si. Set ICE candidate callback\t");
+       g_print("ui. Unset ICE candidate callback\n");
        g_print("co. Create offer\t");
        g_print("ca. Create answer\n");
        g_print("sl. Set local description\t");
        g_print("sr. Set remote description\n");
+       g_print("ac. Add ICE candidate\n");
        g_print("st. Set STUN server\n");
        g_print("----------------------------------- App. Setting ----------------------------------------\n");
        display_setting_status();