From 5b46218f9b70fd9c3d39620bf3d8ee7f7fc24499 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Fri, 13 Oct 2023 17:34:06 +0800 Subject: [PATCH] Add libnice as ice agent --- src/ice/{ => libjuice}/ice_agent.cpp | 2 + src/ice/{ => libjuice}/ice_agent.h | 1 + src/ice/libnice/ice_agent.cpp | 114 ++++++++ src/ice/libnice/ice_agent.h | 66 +++++ src/transmission/ice_transmission.cpp | 69 ++++- src/transmission/ice_transmission.h | 15 +- tests/peerconnection/nice.cpp | 389 ++++++++++++++++++++++++++ tests/peerconnection/nicesdp.cpp | 276 ++++++++++++++++++ xmake.lua | 30 +- 9 files changed, 948 insertions(+), 14 deletions(-) rename src/ice/{ => libjuice}/ice_agent.cpp (99%) rename src/ice/{ => libjuice}/ice_agent.h (97%) create mode 100644 src/ice/libnice/ice_agent.cpp create mode 100644 src/ice/libnice/ice_agent.h create mode 100644 tests/peerconnection/nice.cpp create mode 100644 tests/peerconnection/nicesdp.cpp diff --git a/src/ice/ice_agent.cpp b/src/ice/libjuice/ice_agent.cpp similarity index 99% rename from src/ice/ice_agent.cpp rename to src/ice/libjuice/ice_agent.cpp index fcb1412..48bd921 100644 --- a/src/ice/ice_agent.cpp +++ b/src/ice/libjuice/ice_agent.cpp @@ -69,6 +69,8 @@ int IceAgent::CreateIceAgent(juice_cb_state_changed_t on_state_changed, agent_ = juice_create(&config_); + LOG_INFO("Juice agent init finish"); + return 0; } diff --git a/src/ice/ice_agent.h b/src/ice/libjuice/ice_agent.h similarity index 97% rename from src/ice/ice_agent.h rename to src/ice/libjuice/ice_agent.h index 70be21a..96ea356 100644 --- a/src/ice/ice_agent.h +++ b/src/ice/libjuice/ice_agent.h @@ -4,6 +4,7 @@ #include #include "juice/juice.h" +#include "nice/agent.h" class IceAgent { public: diff --git a/src/ice/libnice/ice_agent.cpp b/src/ice/libnice/ice_agent.cpp new file mode 100644 index 0000000..a78cf47 --- /dev/null +++ b/src/ice/libnice/ice_agent.cpp @@ -0,0 +1,114 @@ +#include "ice_agent.h" + +#include + +#include + +#include "log.h" + +IceAgent::IceAgent(std::string &stun_ip, uint16_t stun_port, + std::string &turn_ip, uint16_t turn_port, + std::string &turn_username, std::string &turn_password) + : stun_ip_(stun_ip), + stun_port_(stun_port), + turn_ip_(turn_ip), + turn_port_(turn_port), + turn_username_(turn_username), + turn_password_(turn_password) {} + +IceAgent::~IceAgent() {} + +int IceAgent::CreateIceAgent(nice_cb_state_changed_t on_state_changed, + nice_cb_candidate_t on_candidate, + nice_cb_gathering_done_t on_gathering_done, + nice_cb_recv_t on_recv, void *user_ptr) { + g_networking_init(); + + gloop_ = g_main_loop_new(NULL, FALSE); + // Create the nice agent_ + agent_ = nice_agent_new(g_main_loop_get_context(gloop_), + NICE_COMPATIBILITY_RFC5245); + if (agent_ == NULL) { + LOG_ERROR("Failed to create agent_"); + } + + g_object_set(agent_, "stun-server", stun_ip_.c_str(), NULL); + g_object_set(agent_, "stun-server-port", stun_port_, NULL); + + g_object_set(agent_, "controlling-mode", controlling_, NULL); + + // Connect to the signals + g_signal_connect(agent_, "candidate-gathering-done", + G_CALLBACK(on_gathering_done), NULL); + g_signal_connect(agent_, "new-selected-pair", G_CALLBACK(on_candidate), NULL); + g_signal_connect(agent_, "component-state-changed", + G_CALLBACK(on_state_changed), NULL); + + // Create a new stream with one component + stream_id_ = nice_agent_add_stream(agent_, 1); + if (stream_id_ == 0) { + LOG_ERROR("Failed to add stream"); + } + nice_agent_set_stream_name(agent_, stream_id_, "video"); + + // Attach to the component to receive the data + // Without this call, candidates cannot be gathered + nice_agent_attach_recv(agent_, stream_id_, 1, g_main_loop_get_context(gloop_), + on_recv, NULL); + + LOG_INFO("Nice agent init finish"); + + return 0; +} + +int IceAgent::DestoryIceAgent() { + g_object_unref(agent_); + return 0; +} + +char *IceAgent::GenerateLocalSdp() { + if (nullptr == agent_) { + LOG_INFO("agent_ is nullptr"); + return nullptr; + } + + local_sdp_ = nice_agent_generate_local_sdp(agent_); + // LOG_INFO("Generate local sdp:[\n{}]", local_sdp_); + + return local_sdp_; +} + +int IceAgent::SetRemoteSdp(const char *remote_sdp) { + int ret = nice_agent_parse_remote_sdp(agent_, remote_sdp); + if (ret > 0) { + return 0; + } else { + LOG_ERROR("Failed to parse remote data"); + return -1; + } +} + +int IceAgent::GatherCandidates() { + if (!nice_agent_gather_candidates(agent_, stream_id_)) { + LOG_ERROR("Failed to start candidate gathering"); + return -1; + } + + return 0; +} + +NiceComponentState IceAgent::GetIceState() { + state_ = nice_agent_get_component_state(agent_, stream_id_, 1); + + return state_; +} + +int IceAgent::Send(const char *data, size_t size) { + if (NiceComponentState::NICE_COMPONENT_STATE_READY != + nice_agent_get_component_state(agent_, stream_id_, 1)) { + return -1; + } + + nice_agent_send(agent_, stream_id_, 1, size, data); + return 0; +} \ No newline at end of file diff --git a/src/ice/libnice/ice_agent.h b/src/ice/libnice/ice_agent.h new file mode 100644 index 0000000..561de22 --- /dev/null +++ b/src/ice/libnice/ice_agent.h @@ -0,0 +1,66 @@ +#ifndef _ICE_AGENT_H_ +#define _ICE_AGENT_H_ + +#include + +#include "gio/gnetworking.h" +#include "nice/agent.h" + +#define NICE_MAX_SDP_STRING_LEN 4096 + +typedef void (*nice_cb_state_changed_t)(NiceAgent* agent, guint stream_id, + guint component_id, + NiceComponentState state, + gpointer data); +typedef void (*nice_cb_candidate_t)(NiceAgent* agent, guint stream_id, + guint component_id, const char* sdp, + gpointer data); +typedef void (*nice_cb_gathering_done_t)(NiceAgent* agent, guint stream_id, + gpointer data); +typedef void (*nice_cb_recv_t)(NiceAgent* agent, guint stream_id, + guint component_id, guint size, gchar* buffer, + gpointer data); + +class IceAgent { + public: + IceAgent(std::string& stun_ip, uint16_t stun_port, std::string& turn_ip, + uint16_t turn_port, std::string& turn_username, + std::string& turn_password); + ~IceAgent(); + + int CreateIceAgent(nice_cb_state_changed_t on_state_changed, + nice_cb_candidate_t on_candidate, + nice_cb_gathering_done_t on_gathering_done, + nice_cb_recv_t on_recv, void* user_ptr); + + int DestoryIceAgent(); + + char* GenerateLocalSdp(); + + int SetRemoteSdp(const char* remote_sdp); + + int GatherCandidates(); + + NiceComponentState GetIceState(); + + int SetRemoteGatheringDone(); + + int Send(const char* data, size_t size); + + private: + std::string stun_ip_ = ""; + uint16_t stun_port_ = 0; + std::string turn_ip_ = ""; + uint16_t turn_port_ = 0; + std::string turn_username_ = ""; + std::string turn_password_ = ""; + NiceAgent* agent_ = nullptr; + GMainLoop* gloop_; + bool controlling_ = false; + uint32_t stream_id_ = 0; + // char local_sdp_[NICE_MAX_SDP_STRING_LEN]; + char* local_sdp_ = nullptr; + NiceComponentState state_; +}; + +#endif \ No newline at end of file diff --git a/src/transmission/ice_transmission.cpp b/src/transmission/ice_transmission.cpp index 23fe317..5660ec3 100644 --- a/src/transmission/ice_transmission.cpp +++ b/src/transmission/ice_transmission.cpp @@ -114,6 +114,63 @@ int IceTransmission::InitIceTransmission(std::string &stun_ip, int stun_port, ice_agent_ = std::make_unique( stun_ip, stun_port, turn_ip, turn_port, turn_username, turn_password); +#ifdef USE_NICE + ice_agent_->CreateIceAgent( + [](NiceAgent *agent, guint stream_id, guint component_id, + NiceComponentState state, gpointer user_ptr) { + if (user_ptr) { + IceTransmission *ice_transmission_obj = + static_cast(user_ptr); + LOG_INFO("[{}->{}] state_change: {}", ice_transmission_obj->user_id_, + ice_transmission_obj->remote_user_id_, + nice_component_state_to_string(state)); + ice_transmission_obj->state_ = state; + ice_transmission_obj->on_ice_status_change_( + nice_component_state_to_string(state)); + } else { + LOG_INFO("state_change: {}", nice_component_state_to_string(state)); + } + }, + [](NiceAgent *agent, guint stream_id, guint component_id, const char *sdp, + gpointer user_ptr) { LOG_INFO("candadite: {}", sdp); }, + [](NiceAgent *agent, guint stream_id, gpointer user_ptr) { + // non-trickle + if (user_ptr) { + IceTransmission *ice_transmission_obj = + static_cast(user_ptr); + LOG_INFO("[{}] gather_done", ice_transmission_obj->user_id_); + + if (ice_transmission_obj->offer_peer_) { + ice_transmission_obj->GetLocalSdp(); + ice_transmission_obj->SendOffer(); + + } else { + ice_transmission_obj->CreateAnswer(); + ice_transmission_obj->SendAnswer(); + } + } + }, + [](NiceAgent *agent, guint stream_id, guint component_id, guint size, + gchar *buffer, gpointer user_ptr) { + if (user_ptr) { + IceTransmission *ice_transmission_obj = + static_cast(user_ptr); + if (ice_transmission_obj) { + if (ice_transmission_obj->CheckIsVideoPacket(buffer, size)) { + RtpPacket packet((uint8_t *)buffer, size); + ice_transmission_obj->rtp_video_receiver_->InsertRtpPacket( + packet); + } else if (ice_transmission_obj->CheckIsDataPacket(buffer, size)) { + RtpPacket packet((uint8_t *)buffer, size); + ice_transmission_obj->rtp_data_receiver_->InsertRtpPacket(packet); + } else if (ice_transmission_obj->CheckIsRtcpPacket(buffer, size)) { + // LOG_ERROR("Rtcp packet [{}]", (uint8_t)(buffer[1])); + } + } + } + }, + this); +#else ice_agent_->CreateIceAgent( [](juice_agent_t *agent, juice_state_t state, void *user_ptr) { if (user_ptr) { @@ -172,6 +229,7 @@ int IceTransmission::InitIceTransmission(std::string &stun_ip, int stun_port, } }, this); +#endif return 0; } @@ -216,11 +274,6 @@ int IceTransmission::SetRemoteSdp(const std::string &remote_sdp) { return 0; } -int IceTransmission::AddRemoteCandidate(const std::string &remote_candidate) { - ice_agent_->AddRemoteCandidates(remote_candidate.c_str()); - return 0; -} - int IceTransmission::CreateOffer() { LOG_INFO("[{}] create offer", user_id_); GatherCandidates(); @@ -262,7 +315,11 @@ int IceTransmission::SendAnswer() { } int IceTransmission::SendData(DATA_TYPE type, const char *data, size_t size) { - if (JUICE_STATE_COMPLETED == state_) { +#ifdef USE_NICE + if (NiceComponentState::NICE_COMPONENT_STATE_READY == state_) { +#else + if (juice_state_t::JUICE_STATE_COMPLETED == state_) { +#endif std::vector packets; if (DATA_TYPE::VIDEO == type) { diff --git a/src/transmission/ice_transmission.h b/src/transmission/ice_transmission.h index f58c454..d473150 100644 --- a/src/transmission/ice_transmission.h +++ b/src/transmission/ice_transmission.h @@ -4,7 +4,6 @@ #include #include "congestion_control.h" -#include "ice_agent.h" #include "ringbuffer.h" #include "rtp_codec.h" #include "rtp_data_receiver.h" @@ -14,6 +13,14 @@ #include "rtp_video_sender.h" #include "ws_transmission.h" +#define USE_NICE 1 + +#ifdef USE_NICE +#include "libnice/ice_agent.h" +#else +#include "libjuice/ice_agent.h" +#endif + class IceTransmission { public: typedef enum { VIDEO = 96, AUDIO = 97, DATA = 127 } DATA_TYPE; @@ -64,8 +71,6 @@ class IceTransmission { int SetRemoteSdp(const std::string &remote_sdp); - int AddRemoteCandidate(const std::string &remote_candidate); - int CreateOffer(); int SendOffer(); @@ -101,7 +106,11 @@ class IceTransmission { std::string remote_user_id_ = ""; bool offer_peer_ = true; std::string remote_ice_username_ = ""; +#ifdef USE_NICE + NiceComponentState state_ = NICE_COMPONENT_STATE_DISCONNECTED; +#else juice_state_t state_ = JUICE_STATE_DISCONNECTED; +#endif private: std::unique_ptr video_rtp_codec_ = nullptr; diff --git a/tests/peerconnection/nice.cpp b/tests/peerconnection/nice.cpp new file mode 100644 index 0000000..e5a1666 --- /dev/null +++ b/tests/peerconnection/nice.cpp @@ -0,0 +1,389 @@ +/* + * Copyright 2013 University of Chicago + * Contact: Bryce Allen + * Copyright 2013 Collabora Ltd. + * Contact: Youness Alaoui + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * Alternatively, the contents of this file may be used under the terms of the + * the GNU Lesser General Public License Version 2.1 (the "LGPL"), in which + * case the provisions of LGPL are applicable instead of those above. If you + * wish to allow use of your version of this file only under the terms of the + * LGPL and not to allow others to use your version of this file under the + * MPL, indicate your decision by deleting the provisions above and replace + * them with the notice and other provisions required by the LGPL. If you do + * not delete the provisions above, a recipient may use your version of this + * file under either the MPL or the LGPL. + */ + +/* + * Example using libnice to negotiate a UDP connection between two clients, + * possibly on the same network or behind different NATs and/or stateful + * firewalls. + * + * Build: + * gcc -o simple-example simple-example.c `pkg-config --cflags --libs nice` + * + * Run two clients, one controlling and one controlled: + * simple-example 0 $(host -4 -t A stun.stunprotocol.org | awk '{ print $4 }') + * simple-example 1 $(host -4 -t A stun.stunprotocol.org | awk '{ print $4 }') + */ +#include +#include +#include +#include + +#include + +#include "gio/gnetworking.h" +#include "nice/agent.h" + +static GMainLoop *gloop; +static GIOChannel *io_stdin; +static guint stream_id; + +static const gchar *candidate_type_name[] = {"host", "srflx", "prflx", "relay"}; + +static const gchar *state_name[] = {"disconnected", "gathering", "connecting", + "connected", "ready", "failed"}; + +static int print_local_data(NiceAgent *agent, guint stream_id, + guint component_id); +static int parse_remote_data(NiceAgent *agent, guint stream_id, + guint component_id, char *line); +static void cb_candidate_gathering_done(NiceAgent *agent, guint stream_id, + gpointer data); +static void cb_new_selected_pair(NiceAgent *agent, guint stream_id, + guint component_id, gchar *lfoundation, + gchar *rfoundation, gpointer data); +static void cb_component_state_changed(NiceAgent *agent, guint stream_id, + guint component_id, guint state, + gpointer data); +static void cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_id, + guint len, gchar *buf, gpointer data); +static gboolean stdin_remote_info_cb(GIOChannel *source, GIOCondition cond, + gpointer data); +static gboolean stdin_send_data_cb(GIOChannel *source, GIOCondition cond, + gpointer data); + +int main(int argc, char *argv[]) { + NiceAgent *agent; + gchar stun_addr[20] = "120.77.216.215"; + guint stun_port = 3478; + gboolean controlling; + + controlling = argv[1][0] - '0'; + if (controlling != 0 && controlling != 1) { + fprintf(stderr, "Usage: %s 0|1 stun_addr [stun_port]\n", argv[0]); + return EXIT_FAILURE; + } + + g_networking_init(); + + gloop = g_main_loop_new(NULL, FALSE); +#ifdef G_OS_WIN32 + io_stdin = g_io_channel_win32_new_fd(_fileno(stdin)); +#else + io_stdin = g_io_channel_unix_new(fileno(stdin)); +#endif + + // Create the nice agent + agent = nice_agent_new(g_main_loop_get_context(gloop), + NICE_COMPATIBILITY_RFC5245); + if (agent == NULL) g_error("Failed to create agent"); + + // Set the STUN settings and controlling mode + if (stun_addr) { + g_object_set(agent, "stun-server", stun_addr, NULL); + g_object_set(agent, "stun-server-port", stun_port, NULL); + } + g_object_set(agent, "controlling-mode", controlling, NULL); + + // Connect to the signals + g_signal_connect(agent, "candidate-gathering-done", + G_CALLBACK(cb_candidate_gathering_done), NULL); + g_signal_connect(agent, "new-selected-pair", G_CALLBACK(cb_new_selected_pair), + NULL); + g_signal_connect(agent, "component-state-changed", + G_CALLBACK(cb_component_state_changed), NULL); + + // Create a new stream with one component + stream_id = nice_agent_add_stream(agent, 1); + if (stream_id == 0) g_error("Failed to add stream"); + + // Attach to the component to receive the data + // Without this call, candidates cannot be gathered + nice_agent_attach_recv(agent, stream_id, 1, g_main_loop_get_context(gloop), + cb_nice_recv, NULL); + + // Start gathering local candidates + if (!nice_agent_gather_candidates(agent, stream_id)) + g_error("Failed to start candidate gathering"); + + g_debug("waiting for candidate-gathering-done signal..."); + + // Run the mainloop. Everything else will happen asynchronously + // when the candidates are done gathering. + g_main_loop_run(gloop); + + g_main_loop_unref(gloop); + g_object_unref(agent); + g_io_channel_unref(io_stdin); + + return EXIT_SUCCESS; +} + +static void cb_candidate_gathering_done(NiceAgent *agent, guint _stream_id, + gpointer data) { + g_debug("SIGNAL candidate gathering done\n"); + + // Candidate gathering is done. Send our local candidates on stdout + printf("Copy this line to remote client:\n"); + printf("\n "); + print_local_data(agent, _stream_id, 1); + printf("\n"); + + // Listen on stdin for the remote candidate list + printf("Enter remote data (single line, no wrapping):\n"); + g_io_add_watch(io_stdin, G_IO_IN, stdin_remote_info_cb, agent); + printf("> "); + fflush(stdout); +} + +static gboolean stdin_remote_info_cb(GIOChannel *source, GIOCondition cond, + gpointer data) { + NiceAgent *agent = (NiceAgent *)data; + gchar *line = NULL; + int rval; + gboolean ret = TRUE; + char cands[5120]; + + std::cin >> cands; + + if (1) { + // Parse remote candidate list and set it on the agent + rval = parse_remote_data(agent, stream_id, 1, cands); + if (rval == EXIT_SUCCESS) { + // Return FALSE so we stop listening to stdin since we parsed the + // candidates correctly + ret = FALSE; + g_debug("waiting for state READY or FAILED signal..."); + } else { + fprintf(stderr, "ERROR: failed to parse remote data\n"); + printf("Enter remote data (single line, no wrapping):\n"); + printf("> "); + fflush(stdout); + } + // g_free(line); + } + + return ret; +} + +static void cb_component_state_changed(NiceAgent *agent, guint _stream_id, + guint component_id, guint state, + gpointer data) { + g_debug("SIGNAL: state changed %d %d %s[%d]\n", _stream_id, component_id, + state_name[state], state); + + if (state == NICE_COMPONENT_STATE_CONNECTED) { + NiceCandidate *local, *remote; + + // Get current selected candidate pair and print IP address used + if (nice_agent_get_selected_pair(agent, _stream_id, component_id, &local, + &remote)) { + gchar ipaddr[INET6_ADDRSTRLEN]; + + nice_address_to_string(&local->addr, ipaddr); + printf("\nNegotiation complete: ([%s]:%d,", ipaddr, + nice_address_get_port(&local->addr)); + nice_address_to_string(&remote->addr, ipaddr); + printf(" [%s]:%d)\n", ipaddr, nice_address_get_port(&remote->addr)); + } + + // Listen to stdin and send data written to it + printf("\nSend lines to remote (Ctrl-D to quit):\n"); + g_io_add_watch(io_stdin, G_IO_IN, stdin_send_data_cb, agent); + printf("> "); + fflush(stdout); + } else if (state == NICE_COMPONENT_STATE_FAILED) { + g_main_loop_quit(gloop); + } +} + +static gboolean stdin_send_data_cb(GIOChannel *source, GIOCondition cond, + gpointer data) { + NiceAgent *agent = (NiceAgent *)data; + gchar *line = NULL; + + if (g_io_channel_read_line(source, &line, NULL, NULL, NULL) == + G_IO_STATUS_NORMAL) { + nice_agent_send(agent, stream_id, 1, strlen(line), line); + g_free(line); + printf("> "); + fflush(stdout); + } else { + nice_agent_send(agent, stream_id, 1, 1, "\0"); + // Ctrl-D was pressed. + g_main_loop_quit(gloop); + } + + return TRUE; +} + +static void cb_new_selected_pair(NiceAgent *agent, guint _stream_id, + guint component_id, gchar *lfoundation, + gchar *rfoundation, gpointer data) { + g_debug("SIGNAL: selected pair %s %s", lfoundation, rfoundation); +} + +static void cb_nice_recv(NiceAgent *agent, guint _stream_id, guint component_id, + guint len, gchar *buf, gpointer data) { + if (len == 1 && buf[0] == '\0') g_main_loop_quit(gloop); + printf("%.*s", len, buf); + fflush(stdout); +} + +static NiceCandidate *parse_candidate(char *scand, guint _stream_id) { + NiceCandidate *cand = NULL; + NiceCandidateType ntype = NICE_CANDIDATE_TYPE_HOST; + gchar **tokens = NULL; + guint i; + + tokens = g_strsplit(scand, ",", 5); + for (i = 0; tokens[i]; i++) + ; + if (i != 5) goto end; + + for (i = 0; i < G_N_ELEMENTS(candidate_type_name); i++) { + if (strcmp(tokens[4], candidate_type_name[i]) == 0) { + ntype = (NiceCandidateType)i; + break; + } + } + if (i == G_N_ELEMENTS(candidate_type_name)) goto end; + + cand = nice_candidate_new(ntype); + cand->component_id = 1; + cand->stream_id = _stream_id; + cand->transport = NICE_CANDIDATE_TRANSPORT_UDP; + strncpy(cand->foundation, tokens[0], NICE_CANDIDATE_MAX_FOUNDATION - 1); + cand->foundation[NICE_CANDIDATE_MAX_FOUNDATION - 1] = 0; + cand->priority = atoi(tokens[1]); + + if (!nice_address_set_from_string(&cand->addr, tokens[2])) { + g_message("failed to parse addr: %s", tokens[2]); + nice_candidate_free(cand); + cand = NULL; + goto end; + } + + nice_address_set_port(&cand->addr, atoi(tokens[3])); + +end: + g_strfreev(tokens); + + return cand; +} + +static int print_local_data(NiceAgent *agent, guint _stream_id, + guint component_id) { + int result = EXIT_FAILURE; + gchar *local_ufrag = NULL; + gchar *local_password = NULL; + gchar ipaddr[INET6_ADDRSTRLEN]; + GSList *cands = NULL, *item; + + if (!nice_agent_get_local_credentials(agent, _stream_id, &local_ufrag, + &local_password)) + goto end; + + cands = nice_agent_get_local_candidates(agent, _stream_id, component_id); + if (cands == NULL) goto end; + + printf("%s %s", local_ufrag, local_password); + + for (item = cands; item; item = item->next) { + NiceCandidate *c = (NiceCandidate *)item->data; + + nice_address_to_string(&c->addr, ipaddr); + + // (foundation),(prio),(addr),(port),(type) + printf(" %s,%u,%s,%u,%s", c->foundation, c->priority, ipaddr, + nice_address_get_port(&c->addr), candidate_type_name[c->type]); + } + printf("\n"); + result = EXIT_SUCCESS; + +end: + if (local_ufrag) g_free(local_ufrag); + if (local_password) g_free(local_password); + if (cands) g_slist_free_full(cands, (GDestroyNotify)&nice_candidate_free); + + return result; +} + +static int parse_remote_data(NiceAgent *agent, guint _stream_id, + guint component_id, char *line) { + GSList *remote_candidates = NULL; + gchar **line_argv = NULL; + const gchar *ufrag = NULL; + const gchar *passwd = NULL; + int result = EXIT_FAILURE; + int i; + + line_argv = g_strsplit_set(line, " \t\n", 0); + for (i = 0; line_argv && line_argv[i]; i++) { + if (strlen(line_argv[i]) == 0) continue; + + // first two args are remote ufrag and password + if (!ufrag) { + ufrag = line_argv[i]; + } else if (!passwd) { + passwd = line_argv[i]; + } else { + // Remaining args are serialized canidates (at least one is required) + NiceCandidate *c = parse_candidate(line_argv[i], _stream_id); + + if (c == NULL) { + g_message("failed to parse candidate: %s", line_argv[i]); + goto end; + } + remote_candidates = g_slist_prepend(remote_candidates, c); + } + } + if (ufrag == NULL || passwd == NULL || remote_candidates == NULL) { + g_message("line must have at least ufrag, password, and one candidate"); + goto end; + } + + if (!nice_agent_set_remote_credentials(agent, _stream_id, ufrag, passwd)) { + g_message("failed to set remote credentials"); + goto end; + } + + // Note: this will trigger the start of negotiation. + if (nice_agent_set_remote_candidates(agent, _stream_id, component_id, + remote_candidates) < 1) { + g_message("failed to set remote candidates"); + goto end; + } + + result = EXIT_SUCCESS; + +end: + if (line_argv != NULL) g_strfreev(line_argv); + if (remote_candidates != NULL) + g_slist_free_full(remote_candidates, (GDestroyNotify)&nice_candidate_free); + + return result; +} diff --git a/tests/peerconnection/nicesdp.cpp b/tests/peerconnection/nicesdp.cpp new file mode 100644 index 0000000..f52a554 --- /dev/null +++ b/tests/peerconnection/nicesdp.cpp @@ -0,0 +1,276 @@ +/* + * Copyright 2013 University of Chicago + * Contact: Bryce Allen + * Copyright 2013 Collabora Ltd. + * Contact: Youness Alaoui + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * Alternatively, the contents of this file may be used under the terms of the + * the GNU Lesser General Public License Version 2.1 (the "LGPL"), in which + * case the provisions of LGPL are applicable instead of those above. If you + * wish to allow use of your version of this file only under the terms of the + * LGPL and not to allow others to use your version of this file under the + * MPL, indicate your decision by deleting the provisions above and replace + * them with the notice and other provisions required by the LGPL. If you do + * not delete the provisions above, a recipient may use your version of this + * file under either the MPL or the LGPL. + */ + +/* + * Example using libnice to negotiate a UDP connection between two clients, + * possibly on the same network or behind different NATs and/or stateful + * firewalls. + * + * Build: + * gcc -o sdp-example sdp-example.c `pkg-config --cflags --libs nice` + * + * Run two clients, one controlling and one controlled: + * sdp-example 0 $(host -4 -t A stun.stunprotocol.org | awk '{ print $4 }') + * sdp-example 1 $(host -4 -t A stun.stunprotocol.org | awk '{ print $4 }') + */ + +#include +#include +#include +#include + +#include + +#include "gio/gnetworking.h" +#include "nice/agent.h" + +static GMainLoop *gloop; +static gchar *stun_addr = NULL; +static guint stun_port; +static gboolean controlling; +static gboolean exit_thread, candidate_gathering_done, negotiation_done; +static GMutex gather_mutex, negotiate_mutex; +static GCond gather_cond, negotiate_cond; + +static const gchar *state_name[] = {"disconnected", "gathering", "connecting", + "connected", "ready", "failed"}; + +static void cb_candidate_gathering_done(NiceAgent *agent, guint stream_id, + gpointer data); +static void cb_component_state_changed(NiceAgent *agent, guint stream_id, + guint component_id, guint state, + gpointer data); +static void cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_id, + guint len, gchar *buf, gpointer data); + +static void *example_thread(void *data); + +int main(int argc, char *argv[]) { + GThread *gexamplethread; + + // Parse arguments + // if (argc > 4 || argc < 2 || argv[1][1] != '\0') { + // fprintf(stderr, "Usage: %s 0|1 stun_addr [stun_port]\n", argv[0]); + // return EXIT_FAILURE; + // } + // controlling = argv[1][0] - '0'; + // if (controlling != 0 && controlling != 1) { + // fprintf(stderr, "Usage: %s 0|1 stun_addr [stun_port]\n", argv[0]); + // return EXIT_FAILURE; + // } + + stun_addr = "120.77.216.215"; + + // if (argc > 2) { + // stun_addr = argv[2]; + // if (argc > 3) + // stun_port = atoi(argv[3]); + // else + stun_port = 3478; + + // g_debug("Using stun server '[%s]:%u'\n", stun_addr, stun_port); + // } + + g_networking_init(); + + gloop = g_main_loop_new(NULL, FALSE); + + // Run the mainloop and the example thread + exit_thread = FALSE; + gexamplethread = g_thread_new("example thread", &example_thread, NULL); + g_main_loop_run(gloop); + exit_thread = TRUE; + + g_thread_join(gexamplethread); + g_main_loop_unref(gloop); + + return EXIT_SUCCESS; +} + +static void *example_thread(void *data) { + NiceAgent *agent; + GIOChannel *io_stdin; + guint stream_id; + gchar *line = NULL; + gchar *sdp, *sdp64; + +#ifdef G_OS_WIN32 + io_stdin = g_io_channel_win32_new_fd(_fileno(stdin)); +#else + io_stdin = g_io_channel_unix_new(fileno(stdin)); +#endif + g_io_channel_set_flags(io_stdin, G_IO_FLAG_NONBLOCK, NULL); + + // Create the nice agent + agent = nice_agent_new(g_main_loop_get_context(gloop), + NICE_COMPATIBILITY_RFC5245); + if (agent == NULL) g_error("Failed to create agent"); + + // Set the STUN settings and controlling mode + if (stun_addr) { + g_object_set(agent, "stun-server", stun_addr, NULL); + g_object_set(agent, "stun-server-port", stun_port, NULL); + } + g_object_set(agent, "controlling-mode", controlling, NULL); + + // Connect to the signals + g_signal_connect(agent, "candidate-gathering-done", + G_CALLBACK(cb_candidate_gathering_done), NULL); + g_signal_connect(agent, "component-state-changed", + G_CALLBACK(cb_component_state_changed), NULL); + + // Create a new stream with one component + stream_id = nice_agent_add_stream(agent, 1); + if (stream_id == 0) g_error("Failed to add stream"); + nice_agent_set_stream_name(agent, stream_id, "text"); + + // Attach to the component to receive the data + // Without this call, candidates cannot be gathered + nice_agent_attach_recv(agent, stream_id, 1, g_main_loop_get_context(gloop), + cb_nice_recv, NULL); + + // Start gathering local candidates + if (!nice_agent_gather_candidates(agent, stream_id)) + g_error("Failed to start candidate gathering"); + + g_debug("waiting for candidate-gathering-done signal..."); + + g_mutex_lock(&gather_mutex); + while (!exit_thread && !candidate_gathering_done) + g_cond_wait(&gather_cond, &gather_mutex); + g_mutex_unlock(&gather_mutex); + if (exit_thread) goto end; + + // Candidate gathering is done. Send our local candidates on stdout + sdp = nice_agent_generate_local_sdp(agent); + printf("Generated SDP from agent :\n%s\n\n", sdp); + printf("Copy the following line to remote client:\n"); + sdp64 = g_base64_encode((const guchar *)sdp, strlen(sdp)); + printf("\n %s\n", sdp64); + g_free(sdp); + g_free(sdp64); + + // Listen on stdin for the remote candidate list + printf("Enter remote data (single line, no wrapping):\n"); + printf("> "); + fflush(stdout); + while (!exit_thread) { + // GIOStatus s = g_io_channel_read_line(io_stdin, &line, NULL, NULL, NULL); + GIOStatus s = G_IO_STATUS_NORMAL; + if (s == G_IO_STATUS_NORMAL) { + char sdp_str[5120]; + std::cin >> sdp_str; + gsize sdp_len; + + sdp = (gchar *)g_base64_decode(sdp_str, &sdp_len); + // Parse remote candidate list and set it on the agent + if (sdp && nice_agent_parse_remote_sdp(agent, sdp) > 0) { + g_free(sdp); + // g_free(line); + break; + } else { + fprintf(stderr, "ERROR: failed to parse remote data\n"); + printf("Enter remote data (single line, no wrapping):\n"); + printf("> "); + fflush(stdout); + } + g_free(sdp); + // g_free(line); + } else if (s == G_IO_STATUS_AGAIN) { + g_usleep(100000); + } + } + + g_debug("waiting for state READY or FAILED signal..."); + g_mutex_lock(&negotiate_mutex); + while (!exit_thread && !negotiation_done) + g_cond_wait(&negotiate_cond, &negotiate_mutex); + g_mutex_unlock(&negotiate_mutex); + if (exit_thread) goto end; + + // Listen to stdin and send data written to it + printf("\nSend lines to remote (Ctrl-D to quit):\n"); + printf("> "); + fflush(stdout); + while (!exit_thread) { + GIOStatus s = g_io_channel_read_line(io_stdin, &line, NULL, NULL, NULL); + + if (s == G_IO_STATUS_NORMAL) { + nice_agent_send(agent, stream_id, 1, strlen(line), line); + g_free(line); + printf("> "); + fflush(stdout); + } else if (s == G_IO_STATUS_AGAIN) { + g_usleep(100000); + } else { + // Ctrl-D was pressed. + nice_agent_send(agent, stream_id, 1, 1, "\0"); + break; + } + } + +end: + g_object_unref(agent); + g_io_channel_unref(io_stdin); + g_main_loop_quit(gloop); + + return NULL; +} + +static void cb_candidate_gathering_done(NiceAgent *agent, guint stream_id, + gpointer data) { + g_debug("SIGNAL candidate gathering done\n"); + + g_mutex_lock(&gather_mutex); + candidate_gathering_done = TRUE; + g_cond_signal(&gather_cond); + g_mutex_unlock(&gather_mutex); +} + +static void cb_component_state_changed(NiceAgent *agent, guint stream_id, + guint component_id, guint state, + gpointer data) { + g_debug("SIGNAL: state changed %d %d %s[%d]\n", stream_id, component_id, + state_name[state], state); + + if (state == NICE_COMPONENT_STATE_READY) { + g_mutex_lock(&negotiate_mutex); + negotiation_done = TRUE; + g_cond_signal(&negotiate_cond); + g_mutex_unlock(&negotiate_mutex); + } else if (state == NICE_COMPONENT_STATE_FAILED) { + g_main_loop_quit(gloop); + } +} + +static void cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_id, + guint len, gchar *buf, gpointer data) { + if (len == 1 && buf[0] == '\0') g_main_loop_quit(gloop); + + printf("%.*s", len, buf); + fflush(stdout); +} diff --git a/xmake.lua b/xmake.lua index 0da805d..a535005 100644 --- a/xmake.lua +++ b/xmake.lua @@ -7,7 +7,7 @@ set_languages("c++17") add_rules("mode.release", "mode.debug") -add_requires("asio 1.24.0", "nlohmann_json", "spdlog 1.11.0") +add_requires("asio 1.24.0", "nlohmann_json", "spdlog 1.11.0", "vcpkg::libnice 0.1.21") add_requires("libjuice", {system = false}) if is_os("windows") then @@ -87,10 +87,18 @@ target("rtp") target("ice") set_kind("static") add_deps("log", "common", "ws") - add_packages("asio", "nlohmann_json", "libjuice") - add_files("src/ice/*.cpp") - add_includedirs("src/ws", {public = true}) + add_packages("asio", "nlohmann_json", "libjuice", "vcpkg::libnice") + add_files("src/ice/libjuice/*.cpp") + add_files("src/ice/libnice/*.cpp") + add_includedirs("src/ws", "src/ice/libjuice", "src/ice/libnice", {public = true}) add_includedirs("thirdparty/libjuice/include", {public = true}) + add_includedirs("E:/SourceCode/vcpkg/installed/x64-windows-static/include/glib-2.0", {public = true}) + add_includedirs("E:/SourceCode/vcpkg/installed/x64-windows-static/lib/glib-2.0/include", {public = true}) + add_linkdirs("E:/SourceCode/vcpkg/installed/x64-windows-static/lib") + add_links("nice", "glib-2.0", "gio-2.0", "gmodule-2.0", "gobject-2.0", "gthread-2.0", + "pcre2-8", "pcre2-16", "pcre2-32", "pcre2-posix", + "zlib", "ffi", "libcrypto", "libssl", "intl", "iconv", "charset", "bz2", + "Shell32", "Advapi32", "Dnsapi", "Shlwapi", "Iphlpapi") target("ws") set_kind("static") @@ -192,4 +200,16 @@ target("guest") set_kind("binary") add_deps("projectx") add_files("tests/peerconnection/guest.cpp") - add_includedirs("src/interface") \ No newline at end of file + add_includedirs("src/interface") + +target("nicetest") + set_kind("binary") + add_packages("vcpkg::libnice") + add_files("tests/peerconnection/nicesdp.cpp") + add_includedirs("E:/SourceCode/vcpkg/installed/x64-windows-static/include/glib-2.0") + add_includedirs("E:/SourceCode/vcpkg/installed/x64-windows-static/lib/glib-2.0/include") + add_linkdirs("E:/SourceCode/vcpkg/installed/x64-windows-static/lib") + add_links("nice", "glib-2.0", "gio-2.0", "gmodule-2.0", "gobject-2.0", "gthread-2.0", + "pcre2-8", "pcre2-16", "pcre2-32", "pcre2-posix", + "zlib", "ffi", "libcrypto", "libssl", "intl", "iconv", "charset", "bz2", + "Shell32", "Advapi32", "Dnsapi", "Shlwapi", "Iphlpapi") \ No newline at end of file