gstreamer/webrtc/sendrecv/gst/webrtc-sendrecv.c
Matthew Waters 3a86a37c03 sendrecv: wait until the offer is set before creating answer
Pragmatically, an answer cannot be created until the offer is created as
the answer creation needs information from the offer.  Practically, due
to implementation details, the answer was always queued after the set of
the offer and so the call flow did not matter.

The current code also hid a bug in webrtcbin where ice candidates would be
generated before the answer had been created which is against the JSEP
specification.

Change to the correct call flow for exemplary effect.
2020-05-06 06:01:57 +00:00

831 lines
24 KiB
C

/*
* Demo gstreamer app for negotiating and streaming a sendrecv webrtc stream
* with a browser JS app.
*
* gcc webrtc-sendrecv.c $(pkg-config --cflags --libs gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0) -o webrtc-sendrecv
*
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
*/
#include <gst/gst.h>
#include <gst/sdp/sdp.h>
#define GST_USE_UNSTABLE_API
#include <gst/webrtc/webrtc.h>
/* For signalling */
#include <libsoup/soup.h>
#include <json-glib/json-glib.h>
#include <string.h>
enum AppState
{
APP_STATE_UNKNOWN = 0,
APP_STATE_ERROR = 1, /* generic error */
SERVER_CONNECTING = 1000,
SERVER_CONNECTION_ERROR,
SERVER_CONNECTED, /* Ready to register */
SERVER_REGISTERING = 2000,
SERVER_REGISTRATION_ERROR,
SERVER_REGISTERED, /* Ready to call a peer */
SERVER_CLOSED, /* server connection closed by us or the server */
PEER_CONNECTING = 3000,
PEER_CONNECTION_ERROR,
PEER_CONNECTED,
PEER_CALL_NEGOTIATING = 4000,
PEER_CALL_STARTED,
PEER_CALL_STOPPING,
PEER_CALL_STOPPED,
PEER_CALL_ERROR,
};
static GMainLoop *loop;
static GstElement *pipe1, *webrtc1;
static GObject *send_channel, *receive_channel;
static SoupWebsocketConnection *ws_conn = NULL;
static enum AppState app_state = 0;
static const gchar *peer_id = NULL;
static const gchar *server_url = "wss://webrtc.nirbheek.in:8443";
static gboolean disable_ssl = FALSE;
static gboolean remote_is_offerer = FALSE;
static GOptionEntry entries[] = {
{"peer-id", 0, 0, G_OPTION_ARG_STRING, &peer_id,
"String ID of the peer to connect to", "ID"},
{"server", 0, 0, G_OPTION_ARG_STRING, &server_url,
"Signalling server to connect to", "URL"},
{"disable-ssl", 0, 0, G_OPTION_ARG_NONE, &disable_ssl, "Disable ssl", NULL},
{"remote-offerer", 0, 0, G_OPTION_ARG_NONE, &remote_is_offerer,
"Request that the peer generate the offer and we'll answer", NULL},
{NULL},
};
static gboolean
cleanup_and_quit_loop (const gchar * msg, enum AppState state)
{
if (msg)
g_printerr ("%s\n", msg);
if (state > 0)
app_state = state;
if (ws_conn) {
if (soup_websocket_connection_get_state (ws_conn) ==
SOUP_WEBSOCKET_STATE_OPEN)
/* This will call us again */
soup_websocket_connection_close (ws_conn, 1000, "");
else
g_object_unref (ws_conn);
}
if (loop) {
g_main_loop_quit (loop);
loop = NULL;
}
/* To allow usage as a GSourceFunc */
return G_SOURCE_REMOVE;
}
static gchar *
get_string_from_json_object (JsonObject * object)
{
JsonNode *root;
JsonGenerator *generator;
gchar *text;
/* Make it the root node */
root = json_node_init_object (json_node_alloc (), object);
generator = json_generator_new ();
json_generator_set_root (generator, root);
text = json_generator_to_data (generator, NULL);
/* Release everything */
g_object_unref (generator);
json_node_free (root);
return text;
}
static void
handle_media_stream (GstPad * pad, GstElement * pipe, const char *convert_name,
const char *sink_name)
{
GstPad *qpad;
GstElement *q, *conv, *resample, *sink;
GstPadLinkReturn ret;
g_print ("Trying to handle stream with %s ! %s", convert_name, sink_name);
q = gst_element_factory_make ("queue", NULL);
g_assert_nonnull (q);
conv = gst_element_factory_make (convert_name, NULL);
g_assert_nonnull (conv);
sink = gst_element_factory_make (sink_name, NULL);
g_assert_nonnull (sink);
if (g_strcmp0 (convert_name, "audioconvert") == 0) {
/* Might also need to resample, so add it just in case.
* Will be a no-op if it's not required. */
resample = gst_element_factory_make ("audioresample", NULL);
g_assert_nonnull (resample);
gst_bin_add_many (GST_BIN (pipe), q, conv, resample, sink, NULL);
gst_element_sync_state_with_parent (q);
gst_element_sync_state_with_parent (conv);
gst_element_sync_state_with_parent (resample);
gst_element_sync_state_with_parent (sink);
gst_element_link_many (q, conv, resample, sink, NULL);
} else {
gst_bin_add_many (GST_BIN (pipe), q, conv, sink, NULL);
gst_element_sync_state_with_parent (q);
gst_element_sync_state_with_parent (conv);
gst_element_sync_state_with_parent (sink);
gst_element_link_many (q, conv, sink, NULL);
}
qpad = gst_element_get_static_pad (q, "sink");
ret = gst_pad_link (pad, qpad);
g_assert_cmphex (ret, ==, GST_PAD_LINK_OK);
}
static void
on_incoming_decodebin_stream (GstElement * decodebin, GstPad * pad,
GstElement * pipe)
{
GstCaps *caps;
const gchar *name;
if (!gst_pad_has_current_caps (pad)) {
g_printerr ("Pad '%s' has no caps, can't do anything, ignoring\n",
GST_PAD_NAME (pad));
return;
}
caps = gst_pad_get_current_caps (pad);
name = gst_structure_get_name (gst_caps_get_structure (caps, 0));
if (g_str_has_prefix (name, "video")) {
handle_media_stream (pad, pipe, "videoconvert", "autovideosink");
} else if (g_str_has_prefix (name, "audio")) {
handle_media_stream (pad, pipe, "audioconvert", "autoaudiosink");
} else {
g_printerr ("Unknown pad %s, ignoring", GST_PAD_NAME (pad));
}
}
static void
on_incoming_stream (GstElement * webrtc, GstPad * pad, GstElement * pipe)
{
GstElement *decodebin;
GstPad *sinkpad;
if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC)
return;
decodebin = gst_element_factory_make ("decodebin", NULL);
g_signal_connect (decodebin, "pad-added",
G_CALLBACK (on_incoming_decodebin_stream), pipe);
gst_bin_add (GST_BIN (pipe), decodebin);
gst_element_sync_state_with_parent (decodebin);
sinkpad = gst_element_get_static_pad (decodebin, "sink");
gst_pad_link (pad, sinkpad);
gst_object_unref (sinkpad);
}
static void
send_ice_candidate_message (GstElement * webrtc G_GNUC_UNUSED, guint mlineindex,
gchar * candidate, gpointer user_data G_GNUC_UNUSED)
{
gchar *text;
JsonObject *ice, *msg;
if (app_state < PEER_CALL_NEGOTIATING) {
cleanup_and_quit_loop ("Can't send ICE, not in call", APP_STATE_ERROR);
return;
}
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);
soup_websocket_connection_send_text (ws_conn, text);
g_free (text);
}
static void
send_sdp_to_peer (GstWebRTCSessionDescription * desc)
{
gchar *text;
JsonObject *msg, *sdp;
if (app_state < PEER_CALL_NEGOTIATING) {
cleanup_and_quit_loop ("Can't send SDP to peer, not in call",
APP_STATE_ERROR);
return;
}
text = gst_sdp_message_as_text (desc->sdp);
sdp = json_object_new ();
if (desc->type == GST_WEBRTC_SDP_TYPE_OFFER) {
g_print ("Sending offer:\n%s\n", text);
json_object_set_string_member (sdp, "type", "offer");
} else if (desc->type == GST_WEBRTC_SDP_TYPE_ANSWER) {
g_print ("Sending answer:\n%s\n", text);
json_object_set_string_member (sdp, "type", "answer");
} else {
g_assert_not_reached ();
}
json_object_set_string_member (sdp, "sdp", text);
g_free (text);
msg = json_object_new ();
json_object_set_object_member (msg, "sdp", sdp);
text = get_string_from_json_object (msg);
json_object_unref (msg);
soup_websocket_connection_send_text (ws_conn, text);
g_free (text);
}
/* Offer created by our pipeline, to be sent to the peer */
static void
on_offer_created (GstPromise * promise, gpointer user_data)
{
GstWebRTCSessionDescription *offer = NULL;
const GstStructure *reply;
g_assert_cmphex (app_state, ==, PEER_CALL_NEGOTIATING);
g_assert_cmphex (gst_promise_wait (promise), ==, GST_PROMISE_RESULT_REPLIED);
reply = gst_promise_get_reply (promise);
gst_structure_get (reply, "offer",
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
gst_promise_unref (promise);
promise = gst_promise_new ();
g_signal_emit_by_name (webrtc1, "set-local-description", offer, promise);
gst_promise_interrupt (promise);
gst_promise_unref (promise);
/* Send offer to peer */
send_sdp_to_peer (offer);
gst_webrtc_session_description_free (offer);
}
static void
on_negotiation_needed (GstElement * element, gpointer user_data)
{
app_state = PEER_CALL_NEGOTIATING;
if (remote_is_offerer) {
gchar *msg = g_strdup_printf ("OFFER_REQUEST");
soup_websocket_connection_send_text (ws_conn, msg);
g_free (msg);
} else {
GstPromise *promise;
promise =
gst_promise_new_with_change_func (on_offer_created, user_data, NULL);;
g_signal_emit_by_name (webrtc1, "create-offer", NULL, promise);
}
}
#define STUN_SERVER " stun-server=stun://stun.l.google.com:19302 "
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
#define RTP_CAPS_VP8 "application/x-rtp,media=video,encoding-name=VP8,payload="
static void
data_channel_on_error (GObject * dc, gpointer user_data)
{
cleanup_and_quit_loop ("Data channel error", 0);
}
static void
data_channel_on_open (GObject * dc, gpointer user_data)
{
GBytes *bytes = g_bytes_new ("data", strlen ("data"));
g_print ("data channel opened\n");
g_signal_emit_by_name (dc, "send-string", "Hi! from GStreamer");
g_signal_emit_by_name (dc, "send-data", bytes);
g_bytes_unref (bytes);
}
static void
data_channel_on_close (GObject * dc, gpointer user_data)
{
cleanup_and_quit_loop ("Data channel closed", 0);
}
static void
data_channel_on_message_string (GObject * dc, gchar * str, gpointer user_data)
{
g_print ("Received data channel message: %s\n", str);
}
static void
connect_data_channel_signals (GObject * data_channel)
{
g_signal_connect (data_channel, "on-error",
G_CALLBACK (data_channel_on_error), NULL);
g_signal_connect (data_channel, "on-open", G_CALLBACK (data_channel_on_open),
NULL);
g_signal_connect (data_channel, "on-close",
G_CALLBACK (data_channel_on_close), NULL);
g_signal_connect (data_channel, "on-message-string",
G_CALLBACK (data_channel_on_message_string), NULL);
}
static void
on_data_channel (GstElement * webrtc, GObject * data_channel,
gpointer user_data)
{
connect_data_channel_signals (data_channel);
receive_channel = data_channel;
}
static void
on_ice_gathering_state_notify (GstElement * webrtcbin, GParamSpec * pspec,
gpointer user_data)
{
GstWebRTCICEGatheringState ice_gather_state;
const gchar *new_state = "unknown";
g_object_get (webrtcbin, "ice-gathering-state", &ice_gather_state, NULL);
switch (ice_gather_state) {
case GST_WEBRTC_ICE_GATHERING_STATE_NEW:
new_state = "new";
break;
case GST_WEBRTC_ICE_GATHERING_STATE_GATHERING:
new_state = "gathering";
break;
case GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE:
new_state = "complete";
break;
}
g_print ("ICE gathering state changed to %s\n", new_state);
}
static gboolean
start_pipeline (void)
{
GstStateChangeReturn ret;
GError *error = NULL;
pipe1 =
gst_parse_launch ("webrtcbin bundle-policy=max-bundle name=sendrecv "
STUN_SERVER
"videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay ! "
"queue ! " RTP_CAPS_VP8 "96 ! sendrecv. "
"audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! "
"queue ! " RTP_CAPS_OPUS "97 ! sendrecv. ", &error);
if (error) {
g_printerr ("Failed to parse launch: %s\n", error->message);
g_error_free (error);
goto err;
}
webrtc1 = gst_bin_get_by_name (GST_BIN (pipe1), "sendrecv");
g_assert_nonnull (webrtc1);
/* This is the gstwebrtc entry point where we create the offer and so on. It
* will be called when the pipeline goes to PLAYING. */
g_signal_connect (webrtc1, "on-negotiation-needed",
G_CALLBACK (on_negotiation_needed), NULL);
/* We need to transmit this ICE candidate to the browser via the websockets
* signalling server. Incoming ice candidates from the browser need to be
* added by us too, see on_server_message() */
g_signal_connect (webrtc1, "on-ice-candidate",
G_CALLBACK (send_ice_candidate_message), NULL);
g_signal_connect (webrtc1, "notify::ice-gathering-state",
G_CALLBACK (on_ice_gathering_state_notify), NULL);
gst_element_set_state (pipe1, GST_STATE_READY);
g_signal_emit_by_name (webrtc1, "create-data-channel", "channel", NULL,
&send_channel);
if (send_channel) {
g_print ("Created data channel\n");
connect_data_channel_signals (send_channel);
} else {
g_print ("Could not create data channel, is usrsctp available?\n");
}
g_signal_connect (webrtc1, "on-data-channel", G_CALLBACK (on_data_channel),
NULL);
/* Incoming streams will be exposed via this signal */
g_signal_connect (webrtc1, "pad-added", G_CALLBACK (on_incoming_stream),
pipe1);
/* Lifetime is the same as the pipeline itself */
gst_object_unref (webrtc1);
g_print ("Starting pipeline\n");
ret = gst_element_set_state (GST_ELEMENT (pipe1), GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE)
goto err;
return TRUE;
err:
if (pipe1)
g_clear_object (&pipe1);
if (webrtc1)
webrtc1 = NULL;
return FALSE;
}
static gboolean
setup_call (void)
{
gchar *msg;
if (soup_websocket_connection_get_state (ws_conn) !=
SOUP_WEBSOCKET_STATE_OPEN)
return FALSE;
if (!peer_id)
return FALSE;
g_print ("Setting up signalling server call with %s\n", peer_id);
app_state = PEER_CONNECTING;
msg = g_strdup_printf ("SESSION %s", peer_id);
soup_websocket_connection_send_text (ws_conn, msg);
g_free (msg);
return TRUE;
}
static gboolean
register_with_server (void)
{
gchar *hello;
gint32 our_id;
if (soup_websocket_connection_get_state (ws_conn) !=
SOUP_WEBSOCKET_STATE_OPEN)
return FALSE;
our_id = g_random_int_range (10, 10000);
g_print ("Registering id %i with server\n", our_id);
app_state = SERVER_REGISTERING;
/* Register with the server with a random integer id. Reply will be received
* by on_server_message() */
hello = g_strdup_printf ("HELLO %i", our_id);
soup_websocket_connection_send_text (ws_conn, hello);
g_free (hello);
return TRUE;
}
static void
on_server_closed (SoupWebsocketConnection * conn G_GNUC_UNUSED,
gpointer user_data G_GNUC_UNUSED)
{
app_state = SERVER_CLOSED;
cleanup_and_quit_loop ("Server connection closed", 0);
}
/* Answer created by our pipeline, to be sent to the peer */
static void
on_answer_created (GstPromise * promise, gpointer user_data)
{
GstWebRTCSessionDescription *answer = NULL;
const GstStructure *reply;
g_assert_cmphex (app_state, ==, PEER_CALL_NEGOTIATING);
g_assert_cmphex (gst_promise_wait (promise), ==, GST_PROMISE_RESULT_REPLIED);
reply = gst_promise_get_reply (promise);
gst_structure_get (reply, "answer",
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &answer, NULL);
gst_promise_unref (promise);
promise = gst_promise_new ();
g_signal_emit_by_name (webrtc1, "set-local-description", answer, promise);
gst_promise_interrupt (promise);
gst_promise_unref (promise);
/* Send answer to peer */
send_sdp_to_peer (answer);
gst_webrtc_session_description_free (answer);
}
static void
on_offer_set (GstPromise * promise, gpointer user_data)
{
gst_promise_unref (promise);
promise = gst_promise_new_with_change_func (on_answer_created, NULL, NULL);
g_signal_emit_by_name (webrtc1, "create-answer", NULL, promise);
}
static void
on_offer_received (GstSDPMessage *sdp)
{
GstWebRTCSessionDescription *offer = NULL;
GstPromise *promise;
offer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_OFFER, sdp);
g_assert_nonnull (offer);
/* Set remote description on our pipeline */
{
promise = gst_promise_new_with_change_func (on_offer_set, NULL, NULL);
g_signal_emit_by_name (webrtc1, "set-remote-description", offer,
promise);
}
gst_webrtc_session_description_free (offer);
}
/* One mega message handler for our asynchronous calling mechanism */
static void
on_server_message (SoupWebsocketConnection * conn, SoupWebsocketDataType type,
GBytes * message, gpointer user_data)
{
gchar *text;
switch (type) {
case SOUP_WEBSOCKET_DATA_BINARY:
g_printerr ("Received unknown binary message, ignoring\n");
return;
case SOUP_WEBSOCKET_DATA_TEXT:{
gsize size;
const gchar *data = g_bytes_get_data (message, &size);
/* Convert to NULL-terminated string */
text = g_strndup (data, size);
break;
}
default:
g_assert_not_reached ();
}
/* Server has accepted our registration, we are ready to send commands */
if (g_strcmp0 (text, "HELLO") == 0) {
if (app_state != SERVER_REGISTERING) {
cleanup_and_quit_loop ("ERROR: Received HELLO when not registering",
APP_STATE_ERROR);
goto out;
}
app_state = SERVER_REGISTERED;
g_print ("Registered with server\n");
/* Ask signalling server to connect us with a specific peer */
if (!setup_call ()) {
cleanup_and_quit_loop ("ERROR: Failed to setup call", PEER_CALL_ERROR);
goto out;
}
/* Call has been setup by the server, now we can start negotiation */
} else if (g_strcmp0 (text, "SESSION_OK") == 0) {
if (app_state != PEER_CONNECTING) {
cleanup_and_quit_loop ("ERROR: Received SESSION_OK when not calling",
PEER_CONNECTION_ERROR);
goto out;
}
app_state = PEER_CONNECTED;
/* Start negotiation (exchange SDP and ICE candidates) */
if (!start_pipeline ())
cleanup_and_quit_loop ("ERROR: failed to start pipeline",
PEER_CALL_ERROR);
/* Handle errors */
} else if (g_str_has_prefix (text, "ERROR")) {
switch (app_state) {
case SERVER_CONNECTING:
app_state = SERVER_CONNECTION_ERROR;
break;
case SERVER_REGISTERING:
app_state = SERVER_REGISTRATION_ERROR;
break;
case PEER_CONNECTING:
app_state = PEER_CONNECTION_ERROR;
break;
case PEER_CONNECTED:
case PEER_CALL_NEGOTIATING:
app_state = PEER_CALL_ERROR;
break;
default:
app_state = APP_STATE_ERROR;
}
cleanup_and_quit_loop (text, 0);
/* Look for JSON messages containing SDP and ICE candidates */
} else {
JsonNode *root;
JsonObject *object, *child;
JsonParser *parser = json_parser_new ();
if (!json_parser_load_from_data (parser, text, -1, NULL)) {
g_printerr ("Unknown message '%s', ignoring", text);
g_object_unref (parser);
goto out;
}
root = json_parser_get_root (parser);
if (!JSON_NODE_HOLDS_OBJECT (root)) {
g_printerr ("Unknown json message '%s', ignoring", text);
g_object_unref (parser);
goto out;
}
object = json_node_get_object (root);
/* Check type of JSON message */
if (json_object_has_member (object, "sdp")) {
int ret;
GstSDPMessage *sdp;
const gchar *text, *sdptype;
GstWebRTCSessionDescription *answer;
g_assert_cmphex (app_state, ==, PEER_CALL_NEGOTIATING);
child = json_object_get_object_member (object, "sdp");
if (!json_object_has_member (child, "type")) {
cleanup_and_quit_loop ("ERROR: received SDP without 'type'",
PEER_CALL_ERROR);
goto out;
}
sdptype = json_object_get_string_member (child, "type");
/* In this example, we create the offer and receive one answer by default,
* but it's possible to comment out the offer creation and wait for an offer
* instead, so we handle either here.
*
* See tests/examples/webrtcbidirectional.c in gst-plugins-bad for another
* example how to handle offers from peers and reply with answers using webrtcbin. */
text = json_object_get_string_member (child, "sdp");
ret = gst_sdp_message_new (&sdp);
g_assert_cmphex (ret, ==, GST_SDP_OK);
ret = gst_sdp_message_parse_buffer ((guint8 *) text, strlen (text), sdp);
g_assert_cmphex (ret, ==, GST_SDP_OK);
if (g_str_equal (sdptype, "answer")) {
g_print ("Received answer:\n%s\n", text);
answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER,
sdp);
g_assert_nonnull (answer);
/* Set remote description on our pipeline */
{
GstPromise *promise = gst_promise_new ();
g_signal_emit_by_name (webrtc1, "set-remote-description", answer,
promise);
gst_promise_interrupt (promise);
gst_promise_unref (promise);
}
app_state = PEER_CALL_STARTED;
} else {
g_print ("Received offer:\n%s\n", text);
on_offer_received (sdp);
}
} else if (json_object_has_member (object, "ice")) {
const gchar *candidate;
gint sdpmlineindex;
child = json_object_get_object_member (object, "ice");
candidate = json_object_get_string_member (child, "candidate");
sdpmlineindex = json_object_get_int_member (child, "sdpMLineIndex");
/* Add ice candidate sent by remote peer */
g_signal_emit_by_name (webrtc1, "add-ice-candidate", sdpmlineindex,
candidate);
} else {
g_printerr ("Ignoring unknown JSON message:\n%s\n", text);
}
g_object_unref (parser);
}
out:
g_free (text);
}
static void
on_server_connected (SoupSession * session, GAsyncResult * res,
SoupMessage * msg)
{
GError *error = NULL;
ws_conn = soup_session_websocket_connect_finish (session, res, &error);
if (error) {
cleanup_and_quit_loop (error->message, SERVER_CONNECTION_ERROR);
g_error_free (error);
return;
}
g_assert_nonnull (ws_conn);
app_state = SERVER_CONNECTED;
g_print ("Connected to signalling server\n");
g_signal_connect (ws_conn, "closed", G_CALLBACK (on_server_closed), NULL);
g_signal_connect (ws_conn, "message", G_CALLBACK (on_server_message), NULL);
/* Register with the server so it knows about us and can accept commands */
register_with_server ();
}
/*
* Connect to the signalling server. This is the entrypoint for everything else.
*/
static void
connect_to_websocket_server_async (void)
{
SoupLogger *logger;
SoupMessage *message;
SoupSession *session;
const char *https_aliases[] = { "wss", NULL };
session =
soup_session_new_with_options (SOUP_SESSION_SSL_STRICT, !disable_ssl,
SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
//SOUP_SESSION_SSL_CA_FILE, "/etc/ssl/certs/ca-bundle.crt",
SOUP_SESSION_HTTPS_ALIASES, https_aliases, NULL);
logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
g_object_unref (logger);
message = soup_message_new (SOUP_METHOD_GET, server_url);
g_print ("Connecting to server...\n");
/* Once connected, we will register */
soup_session_websocket_connect_async (session, message, NULL, NULL, NULL,
(GAsyncReadyCallback) on_server_connected, message);
app_state = SERVER_CONNECTING;
}
static gboolean
check_plugins (void)
{
int i;
gboolean ret;
GstPlugin *plugin;
GstRegistry *registry;
const gchar *needed[] = { "opus", "vpx", "nice", "webrtc", "dtls", "srtp",
"rtpmanager", "videotestsrc", "audiotestsrc", NULL
};
registry = gst_registry_get ();
ret = TRUE;
for (i = 0; i < g_strv_length ((gchar **) needed); i++) {
plugin = gst_registry_find_plugin (registry, needed[i]);
if (!plugin) {
g_print ("Required gstreamer plugin '%s' not found\n", needed[i]);
ret = FALSE;
continue;
}
gst_object_unref (plugin);
}
return ret;
}
int
main (int argc, char *argv[])
{
GOptionContext *context;
GError *error = NULL;
context = g_option_context_new ("- gstreamer webrtc sendrecv demo");
g_option_context_add_main_entries (context, entries, NULL);
g_option_context_add_group (context, gst_init_get_option_group ());
if (!g_option_context_parse (context, &argc, &argv, &error)) {
g_printerr ("Error initializing: %s\n", error->message);
return -1;
}
if (!check_plugins ())
return -1;
if (!peer_id) {
g_printerr ("--peer-id is a required argument\n");
return -1;
}
/* Disable ssl when running a localhost server, because
* it's probably a test server with a self-signed certificate */
{
GstUri *uri = gst_uri_from_string (server_url);
if (g_strcmp0 ("localhost", gst_uri_get_host (uri)) == 0 ||
g_strcmp0 ("127.0.0.1", gst_uri_get_host (uri)) == 0)
disable_ssl = TRUE;
gst_uri_unref (uri);
}
loop = g_main_loop_new (NULL, FALSE);
connect_to_websocket_server_async ();
g_main_loop_run (loop);
g_main_loop_unref (loop);
if (pipe1) {
gst_element_set_state (GST_ELEMENT (pipe1), GST_STATE_NULL);
g_print ("Pipeline stopped\n");
gst_object_unref (pipe1);
}
return 0;
}