/* GStreamer * * Unit tests for webrtcbin * * 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 #include #include #include #include #include "../../../ext/webrtc/webrtcsdp.h" #include "../../../ext/webrtc/webrtcsdp.c" #include "../../../ext/webrtc/utils.h" #include "../../../ext/webrtc/utils.c" #define OPUS_RTP_CAPS(pt) "application/x-rtp,payload=" G_STRINGIFY(pt) ",encoding-name=OPUS,media=audio,clock-rate=48000,ssrc=(uint)3384078950" #define VP8_RTP_CAPS(pt) "application/x-rtp,payload=" G_STRINGIFY(pt) ",encoding-name=VP8,media=video,clock-rate=90000,ssrc=(uint)3484078951" #define H264_RTP_CAPS(pt) "application/x-rtp,payload=" G_STRINGIFY(pt) ",encoding-name=H264,media=video,clock-rate=90000,ssrc=(uint)3484078952" #define TEST_IS_OFFER_ELEMENT(t, e) ((((t)->offerror == 1 && (e) == (t)->webrtc1) || ((t)->offerror == 2 && (e) == (t)->webrtc2)) ? TRUE : FALSE) #define TEST_GET_OFFEROR(t) (TEST_IS_OFFER_ELEMENT(t, t->webrtc1) ? (t)->webrtc1 : t->webrtc2) #define TEST_GET_ANSWERER(t) (TEST_IS_OFFER_ELEMENT(t, t->webrtc1) ? (t)->webrtc2 : t->webrtc1) #define TEST_SDP_IS_LOCAL(t, e, d) ((TEST_IS_OFFER_ELEMENT (t, e) ^ ((d)->type == GST_WEBRTC_SDP_TYPE_OFFER)) == 0) typedef enum { STATE_NEW = 1, STATE_NEGOTIATION_NEEDED, STATE_OFFER_CREATED, STATE_LOCAL_OFFER_SET, STATE_REMOTE_OFFER_SET, STATE_ANSWER_CREATED, STATE_LOCAL_ANSWER_SET, STATE_REMOTE_ANSWER_SET, STATE_EOS, STATE_ERROR, STATE_CUSTOM, } TestState; struct test_webrtc; typedef void (*OnPadAdded) (struct test_webrtc * t, GstElement * element, GstPad * pad, gpointer user_data); /* basic premise of this is that webrtc1 and webrtc2 are attempting to connect * to each other in various configurations */ struct test_webrtc { GList *harnesses; GstTestClock *test_clock; GThread *thread; GMainLoop *loop; GstBus *bus1; GstBus *bus2; GstElement *webrtc1; GstElement *webrtc2; GMutex lock; GCond cond; GArray *states; guint offerror; gulong error_signal_handler_id; gpointer user_data; GDestroyNotify data_notify; /* *INDENT-OFF* */ void (*on_negotiation_needed) (struct test_webrtc * t, GstElement * element, gpointer user_data); gpointer negotiation_data; GDestroyNotify negotiation_notify; void (*on_ice_candidate) (struct test_webrtc * t, GstElement * element, guint mlineindex, gchar * candidate, GstElement * other, gpointer user_data); gpointer ice_candidate_data; GDestroyNotify ice_candidate_notify; void (*on_offer_created) (struct test_webrtc * t, GstElement * element, GstPromise * promise, gpointer user_data); GstWebRTCSessionDescription *offer_desc; gpointer offer_data; GDestroyNotify offer_notify; void (*on_offer_set) (struct test_webrtc * t, GstElement * element, GstPromise * promise, gpointer user_data); gpointer offer_set_data; GDestroyNotify offer_set_notify; gboolean perform_create_answer; void (*on_answer_created) (struct test_webrtc * t, GstElement * element, GstPromise * promise, gpointer user_data); GstWebRTCSessionDescription *answer_desc; gpointer answer_data; GDestroyNotify answer_notify; void (*on_answer_set) (struct test_webrtc * t, GstElement * element, GstPromise * promise, gpointer user_data); gpointer answer_set_data; GDestroyNotify answer_set_notify; void (*on_prepare_data_channel) (struct test_webrtc * t, GstElement * element, GObject * data_channel, gboolean is_local, gpointer user_data); void (*on_data_channel) (struct test_webrtc * t, GstElement * element, GObject *data_channel, gpointer user_data); gpointer data_channel_data; GDestroyNotify data_channel_notify; OnPadAdded on_pad_added; gpointer pad_added_data; GDestroyNotify pad_added_notify; void (*bus_message) (struct test_webrtc * t, GstBus * bus, GstMessage * msg, gpointer user_data); gpointer bus_data; GDestroyNotify bus_notify; /* *INDENT-ON* */ }; static void test_webrtc_signal_state_unlocked (struct test_webrtc *t, TestState state) { GST_TRACE ("signal state 0x%x", state); g_array_append_val (t->states, state); g_cond_broadcast (&t->cond); } static void test_webrtc_signal_state (struct test_webrtc *t, TestState state) { g_mutex_lock (&t->lock); test_webrtc_signal_state_unlocked (t, state); g_mutex_unlock (&t->lock); } #if 0 static gboolean test_webrtc_state_find_unlocked (struct test_webrtc *t, TestState state, guint * idx) { guint i; for (i = 0; i < t->states->len; i++) { if (state == g_array_index (t->states, TestState, i)) return TRUE; } return FALSE; } #endif static void _on_local_answer_set (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstElement *answerer = TEST_GET_ANSWERER (t); g_mutex_lock (&t->lock); if (t->on_answer_set) t->on_answer_set (t, answerer, promise, t->answer_set_data); test_webrtc_signal_state_unlocked (t, STATE_LOCAL_ANSWER_SET); gst_promise_unref (promise); g_mutex_unlock (&t->lock); } static void _on_remote_answer_set (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstElement *offeror = TEST_GET_OFFEROR (t); g_mutex_lock (&t->lock); if (t->on_answer_set) t->on_answer_set (t, offeror, promise, t->answer_set_data); test_webrtc_signal_state_unlocked (t, STATE_REMOTE_ANSWER_SET); gst_promise_unref (promise); g_mutex_unlock (&t->lock); } static void _on_answer_received (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstElement *offeror = TEST_GET_OFFEROR (t); GstElement *answerer = TEST_GET_ANSWERER (t); const GstStructure *reply; GstWebRTCSessionDescription *answer = NULL; GError *error = NULL; reply = gst_promise_get_reply (promise); if (gst_structure_get (reply, "answer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &answer, NULL)) { gchar *desc = gst_sdp_message_as_text (answer->sdp); GST_INFO ("Created Answer: %s", desc); g_free (desc); } else if (gst_structure_get (reply, "error", G_TYPE_ERROR, &error, NULL)) { GST_INFO ("Creating answer resulted in error: %s", error->message); } else { g_assert_not_reached (); } g_mutex_lock (&t->lock); g_assert (t->answer_desc == NULL); t->answer_desc = answer; if (t->on_answer_created) { t->on_answer_created (t, answerer, promise, t->answer_data); } gst_promise_unref (promise); if (error) goto error; if (t->answer_desc) { promise = gst_promise_new_with_change_func (_on_local_answer_set, t, NULL); g_signal_emit_by_name (answerer, "set-local-description", t->answer_desc, promise); promise = gst_promise_new_with_change_func (_on_remote_answer_set, t, NULL); g_signal_emit_by_name (offeror, "set-remote-description", t->answer_desc, promise); } test_webrtc_signal_state_unlocked (t, STATE_ANSWER_CREATED); g_mutex_unlock (&t->lock); return; error: g_clear_error (&error); test_webrtc_signal_state_unlocked (t, STATE_ERROR); g_mutex_unlock (&t->lock); return; } static void _on_local_offer_set (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstElement *offeror = TEST_GET_OFFEROR (t); g_mutex_lock (&t->lock); if (t->on_offer_set) t->on_offer_set (t, offeror, promise, t->offer_set_data); test_webrtc_signal_state_unlocked (t, STATE_LOCAL_OFFER_SET); gst_promise_unref (promise); g_mutex_unlock (&t->lock); } static void _on_remote_offer_set (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstElement *answerer = TEST_GET_ANSWERER (t); g_mutex_lock (&t->lock); if (t->on_offer_set) t->on_offer_set (t, answerer, promise, t->offer_set_data); test_webrtc_signal_state_unlocked (t, STATE_REMOTE_OFFER_SET); gst_promise_unref (promise); g_mutex_unlock (&t->lock); } static void _on_offer_received (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstElement *offeror = TEST_GET_OFFEROR (t); GstElement *answerer = TEST_GET_ANSWERER (t); const GstStructure *reply; GstWebRTCSessionDescription *offer = NULL; GError *error = NULL; reply = gst_promise_get_reply (promise); if (gst_structure_get (reply, "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL)) { gchar *desc = gst_sdp_message_as_text (offer->sdp); GST_INFO ("Created offer: %s", desc); g_free (desc); } else if (gst_structure_get (reply, "error", G_TYPE_ERROR, &error, NULL)) { GST_INFO ("Creating offer resulted in error: %s", error->message); } else { g_assert_not_reached (); } g_mutex_lock (&t->lock); g_assert (t->offer_desc == NULL); t->offer_desc = offer; if (t->on_offer_created) { t->on_offer_created (t, offeror, promise, t->offer_data); } gst_promise_unref (promise); if (error) goto error; test_webrtc_signal_state_unlocked (t, STATE_OFFER_CREATED); gst_object_ref (offeror); gst_object_ref (answerer); g_mutex_unlock (&t->lock); if (t->offer_desc) { promise = gst_promise_new_with_change_func (_on_local_offer_set, t, NULL); g_signal_emit_by_name (offeror, "set-local-description", t->offer_desc, promise); promise = gst_promise_new_with_change_func (_on_remote_offer_set, t, NULL); g_signal_emit_by_name (answerer, "set-remote-description", t->offer_desc, promise); } if (t->perform_create_answer) { promise = gst_promise_new_with_change_func (_on_answer_received, t, NULL); g_signal_emit_by_name (answerer, "create-answer", NULL, promise); } gst_clear_object (&offeror); gst_clear_object (&answerer); return; error: g_clear_error (&error); test_webrtc_signal_state_unlocked (t, STATE_ERROR); g_mutex_unlock (&t->lock); return; } static gboolean _bus_watch (GstBus * bus, GstMessage * msg, struct test_webrtc *t) { g_mutex_lock (&t->lock); switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_STATE_CHANGED: if (GST_ELEMENT (msg->src) == t->webrtc1 || GST_ELEMENT (msg->src) == t->webrtc2) { GstState old, new, pending; gst_message_parse_state_changed (msg, &old, &new, &pending); { gchar *dump_name = g_strconcat (GST_OBJECT_NAME (msg->src), "-state_changed-", gst_element_state_get_name (old), "_", gst_element_state_get_name (new), NULL); GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (msg->src), GST_DEBUG_GRAPH_SHOW_ALL, dump_name); g_free (dump_name); } } break; case GST_MESSAGE_ERROR:{ GError *err = NULL; gchar *dbg_info = NULL; { gchar *dump_name; dump_name = g_strconcat (GST_OBJECT_NAME (t->webrtc1), "-error", NULL); GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (t->webrtc1), GST_DEBUG_GRAPH_SHOW_ALL, dump_name); g_free (dump_name); dump_name = g_strconcat (GST_OBJECT_NAME (t->webrtc2), "-error", NULL); GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (t->webrtc2), GST_DEBUG_GRAPH_SHOW_ALL, dump_name); g_free (dump_name); } gst_message_parse_error (msg, &err, &dbg_info); GST_WARNING ("ERROR from element %s: %s", GST_OBJECT_NAME (msg->src), err->message); GST_WARNING ("Debugging info: %s", (dbg_info) ? dbg_info : "none"); g_error_free (err); g_free (dbg_info); test_webrtc_signal_state_unlocked (t, STATE_ERROR); break; } case GST_MESSAGE_EOS:{ { gchar *dump_name; dump_name = g_strconcat ("%s-eos", GST_OBJECT_NAME (t->webrtc1), NULL); GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (t->webrtc1), GST_DEBUG_GRAPH_SHOW_ALL, dump_name); g_free (dump_name); dump_name = g_strconcat ("%s-eos", GST_OBJECT_NAME (t->webrtc2), NULL); GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (t->webrtc2), GST_DEBUG_GRAPH_SHOW_ALL, dump_name); g_free (dump_name); } GST_INFO ("EOS received"); test_webrtc_signal_state_unlocked (t, STATE_EOS); break; } default: break; } if (t->bus_message) t->bus_message (t, bus, msg, t->bus_data); g_mutex_unlock (&t->lock); return TRUE; } static void on_channel_error_not_reached (GObject * channel, GError * error, gpointer user_data) { g_assert_not_reached (); } static void on_message_string (GObject * channel, const gchar * str, struct test_webrtc *t); static void on_message_data (GObject * channel, GBytes * data, struct test_webrtc *t); static void have_prepare_data_channel (struct test_webrtc *t, GstElement * element, GObject * data_channel, gboolean is_local, gpointer user_data) { t->error_signal_handler_id = g_signal_connect (data_channel, "on-error", G_CALLBACK (on_channel_error_not_reached), NULL); g_signal_connect (data_channel, "on-message-string", G_CALLBACK (on_message_string), t); g_signal_connect (data_channel, "on-message-data", G_CALLBACK (on_message_data), t); } static void _on_prepare_data_channel (GstElement * webrtc, GObject * data_channel, gboolean is_local, struct test_webrtc *t) { /* We can't lock the test_webrtc mutex here because this callback might be * called from an already locked _on_data_channel thread. This is the case for * the test_data_channel_create_after_negotiate test. */ if (t->on_prepare_data_channel) t->on_prepare_data_channel (t, webrtc, data_channel, is_local, t->data_channel_data); } static void _on_negotiation_needed (GstElement * webrtc, struct test_webrtc *t) { g_mutex_lock (&t->lock); if (t->on_negotiation_needed) t->on_negotiation_needed (t, webrtc, t->negotiation_data); test_webrtc_signal_state_unlocked (t, STATE_NEGOTIATION_NEEDED); g_cond_broadcast (&t->cond); g_mutex_unlock (&t->lock); } static void _on_ice_candidate (GstElement * webrtc, guint mlineindex, gchar * candidate, struct test_webrtc *t) { GstElement *other; g_mutex_lock (&t->lock); other = webrtc == t->webrtc1 ? t->webrtc2 : t->webrtc1; if (t->on_ice_candidate) t->on_ice_candidate (t, webrtc, mlineindex, candidate, other, t->ice_candidate_data); g_signal_emit_by_name (other, "add-ice-candidate", mlineindex, candidate); g_mutex_unlock (&t->lock); } static void _on_pad_added (GstElement * webrtc, GstPad * new_pad, struct test_webrtc *t) { g_mutex_lock (&t->lock); if (t->on_pad_added) t->on_pad_added (t, webrtc, new_pad, t->pad_added_data); g_mutex_unlock (&t->lock); } static void _on_data_channel (GstElement * webrtc, GObject * data_channel, struct test_webrtc *t) { g_mutex_lock (&t->lock); if (t->on_data_channel) t->on_data_channel (t, webrtc, data_channel, t->data_channel_data); g_mutex_unlock (&t->lock); } static void _pad_added_not_reached (struct test_webrtc *t, GstElement * element, GstPad * pad, gpointer user_data) { g_assert_not_reached (); } static void _ice_candidate_not_reached (struct test_webrtc *t, GstElement * element, guint mlineindex, gchar * candidate, GstElement * other, gpointer user_data) { g_assert_not_reached (); } static void _negotiation_not_reached (struct test_webrtc *t, GstElement * element, gpointer user_data) { g_assert_not_reached (); } static void _bus_no_errors (struct test_webrtc *t, GstBus * bus, GstMessage * msg, gpointer user_data) { switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR:{ GError *err = NULL; gchar *dbg = NULL; gst_message_parse_error (msg, &err, &dbg); g_error ("ERROR from element %s: %s (Debugging info: %s)", GST_OBJECT_NAME (msg->src), err->message, (dbg) ? dbg : "none"); g_error_free (err); g_free (dbg); g_assert_not_reached (); break; } default: break; } } static void _offer_answer_not_reached (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { g_assert_not_reached (); } static void _on_prepare_data_channel_not_reached (struct test_webrtc *t, GstElement * element, GObject * data_channel, gboolean is_local, gpointer user_data) { g_assert_not_reached (); } static void _on_data_channel_not_reached (struct test_webrtc *t, GstElement * element, GObject * data_channel, gpointer user_data) { g_assert_not_reached (); } static void _broadcast (struct test_webrtc *t) { g_mutex_lock (&t->lock); g_cond_broadcast (&t->cond); g_mutex_unlock (&t->lock); } static gboolean _unlock_create_thread (GMutex * lock) { g_mutex_unlock (lock); return G_SOURCE_REMOVE; } static gpointer _bus_thread (struct test_webrtc *t) { g_mutex_lock (&t->lock); t->loop = g_main_loop_new (NULL, FALSE); g_idle_add ((GSourceFunc) _unlock_create_thread, &t->lock); g_cond_broadcast (&t->cond); g_main_loop_run (t->loop); g_mutex_lock (&t->lock); g_main_loop_unref (t->loop); t->loop = NULL; g_cond_broadcast (&t->cond); g_mutex_unlock (&t->lock); return NULL; } static void element_added_disable_sync (GstBin * bin, GstBin * sub_bin, GstElement * element, gpointer user_data) { GObjectClass *class = G_OBJECT_GET_CLASS (element); if (g_object_class_find_property (class, "async")) g_object_set (element, "async", FALSE, NULL); if (g_object_class_find_property (class, "sync")) g_object_set (element, "sync", FALSE, NULL); } static struct test_webrtc * test_webrtc_new (void) { struct test_webrtc *ret = g_new0 (struct test_webrtc, 1); ret->on_negotiation_needed = _negotiation_not_reached; ret->on_ice_candidate = _ice_candidate_not_reached; ret->on_pad_added = _pad_added_not_reached; ret->perform_create_answer = TRUE; ret->on_offer_created = _offer_answer_not_reached; ret->on_answer_created = _offer_answer_not_reached; ret->on_prepare_data_channel = _on_prepare_data_channel_not_reached; ret->on_data_channel = _on_data_channel_not_reached; ret->bus_message = _bus_no_errors; ret->offerror = 1; ret->error_signal_handler_id = -1; g_mutex_init (&ret->lock); g_cond_init (&ret->cond); ret->states = g_array_new (FALSE, TRUE, sizeof (TestState)); ret->test_clock = GST_TEST_CLOCK (gst_test_clock_new ()); ret->thread = g_thread_new ("test-webrtc", (GThreadFunc) _bus_thread, ret); g_mutex_lock (&ret->lock); while (!ret->loop) g_cond_wait (&ret->cond, &ret->lock); g_mutex_unlock (&ret->lock); ret->bus1 = gst_bus_new (); ret->bus2 = gst_bus_new (); gst_bus_add_watch (ret->bus1, (GstBusFunc) _bus_watch, ret); gst_bus_add_watch (ret->bus2, (GstBusFunc) _bus_watch, ret); ret->webrtc1 = gst_element_factory_make ("webrtcbin", NULL); ret->webrtc2 = gst_element_factory_make ("webrtcbin", NULL); fail_unless (ret->webrtc1 != NULL && ret->webrtc2 != NULL); gst_element_set_clock (ret->webrtc1, GST_CLOCK (ret->test_clock)); gst_element_set_clock (ret->webrtc2, GST_CLOCK (ret->test_clock)); gst_element_set_bus (ret->webrtc1, ret->bus1); gst_element_set_bus (ret->webrtc2, ret->bus2); g_signal_connect (ret->webrtc1, "deep-element-added", G_CALLBACK (element_added_disable_sync), NULL); g_signal_connect (ret->webrtc2, "deep-element-added", G_CALLBACK (element_added_disable_sync), NULL); g_signal_connect (ret->webrtc1, "on-negotiation-needed", G_CALLBACK (_on_negotiation_needed), ret); g_signal_connect (ret->webrtc2, "on-negotiation-needed", G_CALLBACK (_on_negotiation_needed), ret); g_signal_connect (ret->webrtc1, "on-ice-candidate", G_CALLBACK (_on_ice_candidate), ret); g_signal_connect (ret->webrtc2, "on-ice-candidate", G_CALLBACK (_on_ice_candidate), ret); g_signal_connect (ret->webrtc1, "on-data-channel", G_CALLBACK (_on_data_channel), ret); g_signal_connect (ret->webrtc2, "on-data-channel", G_CALLBACK (_on_data_channel), ret); g_signal_connect (ret->webrtc1, "prepare-data-channel", G_CALLBACK (_on_prepare_data_channel), ret); g_signal_connect (ret->webrtc2, "prepare-data-channel", G_CALLBACK (_on_prepare_data_channel), ret); g_signal_connect (ret->webrtc1, "pad-added", G_CALLBACK (_on_pad_added), ret); g_signal_connect (ret->webrtc2, "pad-added", G_CALLBACK (_on_pad_added), ret); g_signal_connect_swapped (ret->webrtc1, "notify::ice-gathering-state", G_CALLBACK (_broadcast), ret); g_signal_connect_swapped (ret->webrtc2, "notify::ice-gathering-state", G_CALLBACK (_broadcast), ret); g_signal_connect_swapped (ret->webrtc1, "notify::ice-connection-state", G_CALLBACK (_broadcast), ret); g_signal_connect_swapped (ret->webrtc2, "notify::ice-connection-state", G_CALLBACK (_broadcast), ret); return ret; } static void test_webrtc_reset_negotiation (struct test_webrtc *t) { GST_DEBUG ("resetting negotiation"); if (t->offer_desc) gst_webrtc_session_description_free (t->offer_desc); t->offer_desc = NULL; if (t->answer_desc) gst_webrtc_session_description_free (t->answer_desc); t->answer_desc = NULL; test_webrtc_signal_state (t, STATE_NEGOTIATION_NEEDED); } static void test_webrtc_clear_states (struct test_webrtc *t) { GST_DEBUG ("clearing states"); g_array_free (t->states, TRUE); t->states = g_array_new (FALSE, TRUE, sizeof (TestState)); } static void test_webrtc_free (struct test_webrtc *t) { /* Otherwise while one webrtcbin is being destroyed, the other could * generate a signal that calls into the destroyed webrtcbin */ g_signal_handlers_disconnect_by_data (t->webrtc1, t); g_signal_handlers_disconnect_by_data (t->webrtc2, t); g_main_loop_quit (t->loop); g_mutex_lock (&t->lock); while (t->loop) g_cond_wait (&t->cond, &t->lock); g_mutex_unlock (&t->lock); g_thread_join (t->thread); g_object_unref (t->test_clock); gst_bus_remove_watch (t->bus1); gst_bus_remove_watch (t->bus2); gst_bus_set_flushing (t->bus1, TRUE); gst_bus_set_flushing (t->bus2, TRUE); gst_object_unref (t->bus1); gst_object_unref (t->bus2); g_list_free_full (t->harnesses, (GDestroyNotify) gst_harness_teardown); if (t->data_notify) t->data_notify (t->user_data); if (t->negotiation_notify) t->negotiation_notify (t->negotiation_data); if (t->ice_candidate_notify) t->ice_candidate_notify (t->ice_candidate_data); if (t->offer_notify) t->offer_notify (t->offer_data); if (t->offer_set_notify) t->offer_set_notify (t->offer_set_data); if (t->answer_notify) t->answer_notify (t->answer_data); if (t->answer_set_notify) t->answer_set_notify (t->answer_set_data); if (t->pad_added_notify) t->pad_added_notify (t->pad_added_data); if (t->data_channel_notify) t->data_channel_notify (t->data_channel_data); fail_unless_equals_int (GST_STATE_CHANGE_SUCCESS, gst_element_set_state (t->webrtc1, GST_STATE_NULL)); fail_unless_equals_int (GST_STATE_CHANGE_SUCCESS, gst_element_set_state (t->webrtc2, GST_STATE_NULL)); test_webrtc_reset_negotiation (t); gst_object_unref (t->webrtc1); gst_object_unref (t->webrtc2); g_mutex_clear (&t->lock); g_cond_clear (&t->cond); g_array_free (t->states, TRUE); t->states = NULL; g_free (t); } static void test_webrtc_create_offer (struct test_webrtc *t) { GstPromise *promise; GstElement *offeror = TEST_GET_OFFEROR (t); promise = gst_promise_new_with_change_func (_on_offer_received, t, NULL); g_signal_emit_by_name (offeror, "create-offer", NULL, promise); } static TestState test_webrtc_check_for_state_mask_unlocked (struct test_webrtc *t, TestState state) { guint i; GST_LOG ("attempting to check for state mask 0x%x", state); for (i = 0; i < t->states->len; i++) { TestState val = g_array_index (t->states, TestState, i); if (((1 << val) & state) != 0) { GST_DEBUG ("found state 0x%x in wait mask 0x%x at idx %u", val, state, i); g_array_remove_range (t->states, 0, i + 1); return val; } } return 0; } static TestState test_webrtc_check_for_state_mask (struct test_webrtc *t, TestState state) { TestState ret; g_mutex_lock (&t->lock); ret = test_webrtc_check_for_state_mask_unlocked (t, state); g_mutex_unlock (&t->lock); return ret; } static TestState test_webrtc_wait_for_state_mask (struct test_webrtc *t, TestState state) { TestState ret = 0; g_mutex_lock (&t->lock); GST_LOG ("attempting to wait for state mask 0x%x", state); while (TRUE) { ret = test_webrtc_check_for_state_mask_unlocked (t, state); if (ret) break; g_cond_wait (&t->cond, &t->lock); } g_mutex_unlock (&t->lock); return ret; } static TestState test_webrtc_wait_for_answer_error_eos (struct test_webrtc *t) { TestState states = 0; states |= (1 << STATE_REMOTE_ANSWER_SET); states |= (1 << STATE_EOS); states |= (1 << STATE_ERROR); return test_webrtc_wait_for_state_mask (t, states); } static void test_webrtc_wait_for_ice_gathering_complete (struct test_webrtc *t) { GstWebRTCICEGatheringState ice_state1, ice_state2; g_mutex_lock (&t->lock); g_object_get (t->webrtc1, "ice-gathering-state", &ice_state1, NULL); g_object_get (t->webrtc2, "ice-gathering-state", &ice_state2, NULL); while (ice_state1 != GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE || ice_state2 != GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) { g_cond_wait (&t->cond, &t->lock); g_object_get (t->webrtc1, "ice-gathering-state", &ice_state1, NULL); g_object_get (t->webrtc2, "ice-gathering-state", &ice_state2, NULL); } g_mutex_unlock (&t->lock); } #if 0 static void test_webrtc_wait_for_ice_connection (struct test_webrtc *t, GstWebRTCICEConnectionState states) { GstWebRTCICEConnectionState ice_state1, ice_state2, current; g_mutex_lock (&t->lock); g_object_get (t->webrtc1, "ice-connection-state", &ice_state1, NULL); g_object_get (t->webrtc2, "ice-connection-state", &ice_state2, NULL); current = (1 << ice_state1) | (1 << ice_state2); while ((current & states) == 0 || (current & ~states)) { g_cond_wait (&t->cond, &t->lock); g_object_get (t->webrtc1, "ice-connection-state", &ice_state1, NULL); g_object_get (t->webrtc2, "ice-connection-state", &ice_state2, NULL); current = (1 << ice_state1) | (1 << ice_state2); } g_mutex_unlock (&t->lock); } #endif static void _pad_added_fakesink (struct test_webrtc *t, GstElement * element, GstPad * pad, gpointer user_data) { GstHarness *h; if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC) return; h = gst_harness_new_with_element (element, NULL, "src_%u"); gst_harness_add_sink_parse (h, "fakesink async=false sync=false"); t->harnesses = g_list_prepend (t->harnesses, h); } static void on_negotiation_needed_hit (struct test_webrtc *t, GstElement * element, gpointer user_data) { guint *flag = (guint *) user_data; *flag |= 1 << ((element == t->webrtc1) ? 1 : 2); } typedef void (*ValidateSDPFunc) (struct test_webrtc * t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data); struct validate_sdp; struct validate_sdp { ValidateSDPFunc validate; gpointer user_data; struct validate_sdp *next; }; #define VAL_SDP_INIT(name,func,data,next) \ struct validate_sdp name = { func, data, next } static void _check_validate_sdp (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { struct validate_sdp *validate = user_data; GstWebRTCSessionDescription *desc = NULL; if (TEST_IS_OFFER_ELEMENT (t, element)) desc = t->offer_desc; else desc = t->answer_desc; while (validate) { validate->validate (t, element, desc, validate->user_data); validate = validate->next; } } static void test_validate_sdp_full (struct test_webrtc *t, struct validate_sdp *offer, struct validate_sdp *answer, TestState wait_mask, gboolean perform_state_change) { if (offer) { t->offer_data = offer; t->on_offer_created = _check_validate_sdp; } else { t->offer_data = NULL; t->on_offer_created = NULL; } if (answer) { t->answer_data = answer; t->on_answer_created = _check_validate_sdp; } else { t->answer_data = NULL; t->on_answer_created = NULL; } if (perform_state_change) { fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); } test_webrtc_create_offer (t); if (wait_mask == 0) { fail_unless_equals_int (test_webrtc_wait_for_answer_error_eos (t), STATE_REMOTE_ANSWER_SET); } else { test_webrtc_wait_for_state_mask (t, wait_mask); } } static void test_validate_sdp (struct test_webrtc *t, struct validate_sdp *offer, struct validate_sdp *answer) { test_validate_sdp_full (t, offer, answer, 0, TRUE); } static void _count_num_sdp_media (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { guint expected = GPOINTER_TO_UINT (user_data); fail_unless_equals_int (gst_sdp_message_medias_len (desc->sdp), expected); } GST_START_TEST (test_sdp_no_media) { struct test_webrtc *t = test_webrtc_new (); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (0), NULL); /* check that a no stream connection creates 0 media sections */ t->on_negotiation_needed = NULL; test_validate_sdp (t, &count, &count); test_webrtc_free (t); } GST_END_TEST; static void on_sdp_media_direction (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { gchar **expected_directions = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); if (g_strcmp0 (gst_sdp_media_get_media (media), "audio") == 0 || g_strcmp0 (gst_sdp_media_get_media (media), "video") == 0) { gboolean have_direction = FALSE; int j; for (j = 0; j < gst_sdp_media_attributes_len (media); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); if (g_strcmp0 (attr->key, "inactive") == 0) { fail_unless (have_direction == FALSE, "duplicate/multiple directions for media %u", i); have_direction = TRUE; fail_unless_equals_string (attr->key, expected_directions[i]); } else if (g_strcmp0 (attr->key, "sendonly") == 0) { fail_unless (have_direction == FALSE, "duplicate/multiple directions for media %u", i); have_direction = TRUE; fail_unless_equals_string (attr->key, expected_directions[i]); } else if (g_strcmp0 (attr->key, "recvonly") == 0) { fail_unless (have_direction == FALSE, "duplicate/multiple directions for media %u", i); have_direction = TRUE; fail_unless_equals_string (attr->key, expected_directions[i]); } else if (g_strcmp0 (attr->key, "sendrecv") == 0) { fail_unless (have_direction == FALSE, "duplicate/multiple directions for media %u", i); have_direction = TRUE; fail_unless_equals_string (attr->key, expected_directions[i]); } } fail_unless (have_direction, "no direction attribute in media %u", i); } } } static void on_sdp_media_no_duplicate_payloads (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { int i, j, k; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); GArray *media_formats = g_array_new (FALSE, FALSE, sizeof (int)); for (j = 0; j < gst_sdp_media_formats_len (media); j++) { int pt = atoi (gst_sdp_media_get_format (media, j)); for (k = 0; k < media_formats->len; k++) { int val = g_array_index (media_formats, int, k); if (pt == val) fail ("found an unexpected duplicate payload type %u within media %u", pt, i); } g_array_append_val (media_formats, pt); } g_array_free (media_formats, TRUE); } } static void on_sdp_media_count_formats (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { guint *expected_n_media_formats = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); fail_unless_equals_int (gst_sdp_media_formats_len (media), expected_n_media_formats[i]); } } static void on_sdp_media_setup (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { gchar **expected_setup = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); gboolean have_setup = FALSE; int j; for (j = 0; j < gst_sdp_media_attributes_len (media); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); if (g_strcmp0 (attr->key, "setup") == 0) { fail_unless (have_setup == FALSE, "duplicate/multiple setup for media %u", i); have_setup = TRUE; fail_unless_equals_string (attr->value, expected_setup[i]); } } fail_unless (have_setup, "no setup attribute in media %u", i); } } static void add_fake_audio_src_harness (GstHarness * h, gint pt, guint ssrc) { GstCaps *caps = gst_caps_from_string (OPUS_RTP_CAPS (pt)); GstStructure *s = gst_caps_get_structure (caps, 0); if (ssrc != 0) gst_structure_set (s, "ssrc", G_TYPE_UINT, ssrc, NULL); gst_structure_set (s, "payload", G_TYPE_INT, pt, NULL); gst_harness_set_src_caps (h, caps); gst_harness_add_src_parse (h, "fakesrc is-live=true", TRUE); } static void add_fake_video_src_harness (GstHarness * h, gint pt, guint ssrc) { GstCaps *caps = gst_caps_from_string (VP8_RTP_CAPS (pt)); GstStructure *s = gst_caps_get_structure (caps, 0); if (ssrc != 0) gst_structure_set (s, "ssrc", G_TYPE_UINT, ssrc, NULL); gst_structure_set (s, "payload", G_TYPE_INT, pt, NULL); gst_harness_set_src_caps (h, caps); gst_harness_add_src_parse (h, "fakesrc is-live=true", TRUE); } static struct test_webrtc * create_audio_test (void) { struct test_webrtc *t = test_webrtc_new (); GstHarness *h; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; t->on_prepare_data_channel = have_prepare_data_channel; h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); return t; } static void add_audio_test_src_harness (GstHarness * h, guint ssrc); static void add_video_test_src_harness (GstHarness * h, guint ssrc); static void on_new_transceiver_expected_kind (GstWebRTCBin * webrtc, GstWebRTCRTPTransceiver * trans, gpointer user_data) { GstWebRTCKind kind, expected = GPOINTER_TO_UINT (user_data); g_object_get (trans, "kind", &kind, NULL); fail_unless_equals_int (kind, expected); } GST_START_TEST (test_audio) { struct test_webrtc *t = create_audio_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstWebRTCKind expected_kind = GST_WEBRTC_KIND_AUDIO; /* check that a single stream connection creates the associated number * of media sections */ g_signal_connect (t->webrtc1, "on-new-transceiver", G_CALLBACK (on_new_transceiver_expected_kind), GUINT_TO_POINTER (expected_kind)); g_signal_connect (t->webrtc2, "on-new-transceiver", G_CALLBACK (on_new_transceiver_expected_kind), GUINT_TO_POINTER (expected_kind)); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; static void _check_ice_port_restriction (struct test_webrtc *t, GstElement * element, guint mlineindex, gchar * candidate, GstElement * other, gpointer user_data) { GRegex *regex; GMatchInfo *match_info; gchar *candidate_port; gchar *candidate_protocol; gchar *candidate_typ; guint port_as_int; guint peer_number; if (!candidate || candidate[0] == '\0') return; regex = g_regex_new ("candidate:(\\d+) (1) (UDP|TCP) (\\d+) ([0-9.]+|[0-9a-f:]+)" " (\\d+) typ ([a-z]+)", 0, 0, NULL); g_regex_match (regex, candidate, 0, &match_info); fail_unless (g_match_info_get_match_count (match_info) == 8, candidate); candidate_protocol = g_match_info_fetch (match_info, 2); candidate_port = g_match_info_fetch (match_info, 6); candidate_typ = g_match_info_fetch (match_info, 7); peer_number = t->webrtc1 == element ? 1 : 2; port_as_int = atoi (candidate_port); if (!g_strcmp0 (candidate_typ, "host") && port_as_int != 9) { guint expected_min = peer_number * 10000 + 1000; guint expected_max = expected_min + 999; fail_unless (port_as_int >= expected_min); fail_unless (port_as_int <= expected_max); } g_free (candidate_port); g_free (candidate_protocol); g_free (candidate_typ); g_match_info_free (match_info); g_regex_unref (regex); } GST_START_TEST (test_ice_port_restriction) { struct test_webrtc *t = create_audio_test (); GObject *webrtcice; VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); /* * Ports are defined as follows "{peer}{protocol}000" * - peer number: "1" for t->webrtc1, "2" for t->webrtc2 */ g_object_get (t->webrtc1, "ice-agent", &webrtcice, NULL); g_object_set (webrtcice, "min-rtp-port", 11000, "max-rtp-port", 11999, NULL); g_object_unref (webrtcice); g_object_get (t->webrtc2, "ice-agent", &webrtcice, NULL); g_object_set (webrtcice, "min-rtp-port", 21000, "max-rtp-port", 21999, NULL); g_object_unref (webrtcice); t->on_ice_candidate = _check_ice_port_restriction; test_validate_sdp (t, &offer, &answer); test_webrtc_wait_for_ice_gathering_complete (t); test_webrtc_free (t); } GST_END_TEST; static struct test_webrtc * create_audio_video_test (void) { struct test_webrtc *t = create_audio_test (); GstHarness *h; h = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_fake_video_src_harness (h, 97, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h); return t; } GST_START_TEST (test_audio_video) { struct test_webrtc *t = create_audio_video_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); /* check that a dual stream connection creates the associated number * of media sections */ test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_media_direction) { struct test_webrtc *t = create_audio_video_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstHarness *h; /* check the default media directions for transceivers */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; static void on_sdp_media_payload_types (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { const GstSDPMedia *vmedia; guint video_mline = GPOINTER_TO_UINT (user_data); guint j; vmedia = gst_sdp_message_get_media (desc->sdp, video_mline); for (j = 0; j < gst_sdp_media_attributes_len (vmedia); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (vmedia, j); if (!g_strcmp0 (attr->key, "rtpmap")) { if (g_str_has_prefix (attr->value, "97")) { fail_unless_equals_string (attr->value, "97 VP8/90000"); } else if (g_str_has_prefix (attr->value, "96")) { fail_unless_equals_string (attr->value, "96 red/90000"); } else if (g_str_has_prefix (attr->value, "98")) { fail_unless_equals_string (attr->value, "98 ulpfec/90000"); } else if (g_str_has_prefix (attr->value, "99")) { fail_unless_equals_string (attr->value, "99 rtx/90000"); } else if (g_str_has_prefix (attr->value, "100")) { fail_unless_equals_string (attr->value, "100 rtx/90000"); } else if (g_str_has_prefix (attr->value, "101")) { fail_unless_equals_string (attr->value, "101 H264/90000"); } } } } /* In this test we verify that webrtcbin will pick available payload * types when it needs to, in that example for RTX and FEC */ GST_START_TEST (test_payload_types) { struct test_webrtc *t = create_audio_video_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 5, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (payloads, on_sdp_media_payload_types, GUINT_TO_POINTER (1), &media_formats); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &payloads); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); GstWebRTCRTPTransceiver *trans; GArray *transceivers; g_signal_emit_by_name (t->webrtc1, "get-transceivers", &transceivers); fail_unless_equals_int (transceivers->len, 2); trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 1); g_object_set (trans, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, "do-nack", TRUE, NULL); g_array_unref (transceivers); /* We don't really care about the answer here */ test_validate_sdp (t, &offer, NULL); test_webrtc_free (t); } GST_END_TEST; static void _check_transceiver_mids (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { const GArray *expected_mids = user_data; GArray *transceivers; int i; g_signal_emit_by_name (element, "get-transceivers", &transceivers); fail_unless (transceivers != NULL); fail_unless_equals_uint64 (transceivers->len, expected_mids->len); for (i = 0; i < transceivers->len; ++i) { GstWebRTCRTPTransceiver *trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, i); gchar *mid = g_array_index (expected_mids, char *, i); fail_unless_equals_string (trans->mid, mid); } g_array_unref (transceivers); } GST_START_TEST (test_transceivers_mid) { struct test_webrtc *t = create_audio_video_test (); const gchar *EXPECTED_MIDS_DATA[] = { "audio0", "video1" }; GArray *expected_mids = g_array_new (FALSE, FALSE, sizeof (gchar *)); g_array_append_vals (expected_mids, EXPECTED_MIDS_DATA, sizeof (EXPECTED_MIDS_DATA) / sizeof (gchar *)); t->on_offer_set = _check_transceiver_mids; t->offer_set_data = expected_mids; t->on_answer_set = _check_transceiver_mids; t->answer_set_data = expected_mids; test_validate_sdp (t, NULL, NULL); test_webrtc_free (t); g_array_free (expected_mids, TRUE); } GST_END_TEST; GST_START_TEST (test_no_nice_elements_request_pad) { struct test_webrtc *t = test_webrtc_new (); GstPluginFeature *nicesrc, *nicesink; GstRegistry *registry; GstPad *pad; /* check that the absence of libnice elements posts an error on the bus * when requesting a pad */ registry = gst_registry_get (); nicesrc = gst_registry_lookup_feature (registry, "nicesrc"); nicesink = gst_registry_lookup_feature (registry, "nicesink"); if (nicesrc) gst_registry_remove_feature (registry, nicesrc); if (nicesink) gst_registry_remove_feature (registry, nicesink); t->bus_message = NULL; pad = gst_element_request_pad_simple (t->webrtc1, "sink_0"); fail_unless (pad == NULL); fail_unless_equals_int (STATE_ERROR, test_webrtc_wait_for_answer_error_eos (t)); test_webrtc_free (t); if (nicesrc) gst_registry_add_feature (registry, nicesrc); if (nicesink) gst_registry_add_feature (registry, nicesink); } GST_END_TEST; GST_START_TEST (test_no_nice_elements_state_change) { struct test_webrtc *t = test_webrtc_new (); GstPluginFeature *nicesrc, *nicesink; GstRegistry *registry; /* check that the absence of libnice elements posts an error on the bus */ registry = gst_registry_get (); nicesrc = gst_registry_lookup_feature (registry, "nicesrc"); nicesink = gst_registry_lookup_feature (registry, "nicesink"); if (nicesrc) gst_registry_remove_feature (registry, nicesrc); if (nicesink) gst_registry_remove_feature (registry, nicesink); t->bus_message = NULL; gst_element_set_state (t->webrtc1, GST_STATE_READY); fail_unless_equals_int (STATE_ERROR, test_webrtc_wait_for_answer_error_eos (t)); test_webrtc_free (t); if (nicesrc) gst_registry_add_feature (registry, nicesrc); if (nicesink) gst_registry_add_feature (registry, nicesink); } GST_END_TEST; static void validate_rtc_stats (const GstStructure * s) { GstWebRTCStatsType type = 0; double ts = 0.; gchar *id = NULL; fail_unless (gst_structure_get (s, "type", GST_TYPE_WEBRTC_STATS_TYPE, &type, NULL)); fail_unless (gst_structure_get (s, "id", G_TYPE_STRING, &id, NULL)); fail_unless (gst_structure_get (s, "timestamp", G_TYPE_DOUBLE, &ts, NULL)); fail_unless (type != 0); fail_unless (ts != 0.); fail_unless (id != NULL); g_free (id); } static void validate_codec_stats (const GstStructure * s) { guint pt = 0, clock_rate = 0; fail_unless (gst_structure_get (s, "payload-type", G_TYPE_UINT, &pt, NULL)); fail_unless (gst_structure_get (s, "clock-rate", G_TYPE_UINT, &clock_rate, NULL)); fail_unless (pt >= 0 && pt <= 127); fail_unless (clock_rate >= 0); } static void validate_rtc_stream_stats (const GstStructure * s, const GstStructure * stats) { gchar *codec_id, *transport_id, *kind; GstStructure *codec, *transport; guint ssrc; fail_unless (gst_structure_get (s, "codec-id", G_TYPE_STRING, &codec_id, NULL)); fail_unless (gst_structure_get (s, "transport-id", G_TYPE_STRING, &transport_id, NULL)); fail_unless (gst_structure_get (stats, codec_id, GST_TYPE_STRUCTURE, &codec, NULL)); fail_unless (gst_structure_get (stats, transport_id, GST_TYPE_STRUCTURE, &transport, NULL)); fail_unless (codec != NULL); fail_unless (transport != NULL); gst_structure_free (transport); gst_structure_free (codec); fail_unless (gst_structure_get (s, "ssrc", G_TYPE_UINT, &ssrc, NULL)); fail_unless (gst_structure_get (s, "kind", G_TYPE_STRING, &kind, NULL)); // Using ssrc to differentiate video from audio streams is the easiest // way, otherwise we have to pass the info some other way or look up // caps in the codec stats entries. if (ssrc == 0xCAFECAFE) { fail_unless (g_str_equal (kind, "video")); } else { fail_unless (g_str_equal (kind, "audio")); } g_free (codec_id); g_free (transport_id); g_free (kind); } static void validate_inbound_rtp_stats (const GstStructure * s, const GstStructure * stats) { guint fir, pli, nack; gint64 packets_lost; guint64 packets_received, bytes_received; double jitter; gchar *remote_id; GstStructure *remote; validate_rtc_stream_stats (s, stats); fail_unless (gst_structure_get (s, "fir-count", G_TYPE_UINT, &fir, NULL)); fail_unless (gst_structure_get (s, "pli-count", G_TYPE_UINT, &pli, NULL)); fail_unless (gst_structure_get (s, "nack-count", G_TYPE_UINT, &nack, NULL)); fail_unless (gst_structure_get (s, "packets-received", G_TYPE_UINT64, &packets_received, NULL)); fail_unless (gst_structure_get (s, "bytes-received", G_TYPE_UINT64, &bytes_received, NULL)); fail_unless (gst_structure_get (s, "jitter", G_TYPE_DOUBLE, &jitter, NULL)); fail_unless (gst_structure_get (s, "packets-lost", G_TYPE_INT64, &packets_lost, NULL)); fail_unless (gst_structure_get (s, "remote-id", G_TYPE_STRING, &remote_id, NULL)); fail_unless (gst_structure_get (stats, remote_id, GST_TYPE_STRUCTURE, &remote, NULL)); fail_unless (remote != NULL); gst_structure_free (remote); g_free (remote_id); } static void validate_remote_inbound_rtp_stats (const GstStructure * s, const GstStructure * stats) { gint64 packets_lost; double jitter, rtt; gchar *local_id; GstStructure *local; validate_rtc_stream_stats (s, stats); fail_unless (gst_structure_get (s, "jitter", G_TYPE_DOUBLE, &jitter, NULL)); fail_unless (gst_structure_get (s, "packets-lost", G_TYPE_INT64, &packets_lost, NULL)); fail_unless (gst_structure_get (s, "round-trip-time", G_TYPE_DOUBLE, &rtt, NULL)); fail_unless (gst_structure_get (s, "local-id", G_TYPE_STRING, &local_id, NULL)); fail_unless (gst_structure_get (stats, local_id, GST_TYPE_STRUCTURE, &local, NULL)); fail_unless (local != NULL); gst_structure_free (local); g_free (local_id); } static void validate_outbound_rtp_stats (const GstStructure * s, const GstStructure * stats) { guint fir, pli, nack; guint64 packets_sent, bytes_sent; gchar *remote_id; GstStructure *remote; validate_rtc_stream_stats (s, stats); fail_unless (gst_structure_get (s, "fir-count", G_TYPE_UINT, &fir, NULL)); fail_unless (gst_structure_get (s, "pli-count", G_TYPE_UINT, &pli, NULL)); fail_unless (gst_structure_get (s, "nack-count", G_TYPE_UINT, &nack, NULL)); fail_unless (gst_structure_get (s, "packets-sent", G_TYPE_UINT64, &packets_sent, NULL)); fail_unless (gst_structure_get (s, "bytes-sent", G_TYPE_UINT64, &bytes_sent, NULL)); if (gst_structure_get (s, "remote-id", G_TYPE_STRING, &remote_id, NULL)) { fail_unless (gst_structure_get (stats, remote_id, GST_TYPE_STRUCTURE, &remote, NULL)); fail_unless (remote != NULL); gst_structure_free (remote); g_free (remote_id); } } static void validate_remote_outbound_rtp_stats (const GstStructure * s, const GstStructure * stats) { gchar *local_id; GstStructure *local; validate_rtc_stream_stats (s, stats); fail_unless (gst_structure_get (s, "local-id", G_TYPE_STRING, &local_id, NULL)); fail_unless (gst_structure_get (stats, local_id, GST_TYPE_STRUCTURE, &local, NULL)); fail_unless (local != NULL); gst_structure_free (local); g_free (local_id); } static void validate_candidate_stats (const GstStructure * s, const GstStructure * stats) { guint port; guint64 priority; gchar *address, *candidateType, *protocol; fail_unless (gst_structure_get (s, "address", G_TYPE_STRING, &address, NULL)); fail_unless (gst_structure_get (s, "port", G_TYPE_UINT, &port, NULL)); fail_unless (gst_structure_get (s, "candidate-type", G_TYPE_STRING, &candidateType, NULL)); fail_unless (gst_structure_get (s, "priority", G_TYPE_UINT, &priority, NULL)); fail_unless (gst_structure_get (s, "protocol", G_TYPE_STRING, &protocol, NULL)); fail_unless (strcmp (protocol, "udp") || strcmp (protocol, "tcp")); g_free (address); g_free (candidateType); g_free (protocol); } static void validate_peer_connection_stats (const GstStructure * s) { guint opened, closed; fail_unless (gst_structure_get (s, "data-channels-opened", G_TYPE_UINT, &opened, NULL)); fail_unless (gst_structure_get (s, "data-channels-closed", G_TYPE_UINT, &closed, NULL)); fail_unless (opened >= closed); } struct stats_check_state { struct test_webrtc *t; gint n_streams; const GstStructure *stats; gboolean saw_outbound_rtp; gboolean saw_remote_outbound_rtp; gboolean saw_inbound_rtp; gboolean saw_remote_inbound_rtp; }; static gboolean validate_stats_foreach (const GstIdStr * fieldname, const GValue * value, gpointer user_data) { struct stats_check_state *state = (struct stats_check_state *) (user_data); const GstStructure *stats = state->stats; const gchar *field = gst_id_str_as_str (fieldname); GstWebRTCStatsType type; fail_unless (GST_VALUE_HOLDS_STRUCTURE (value)); const GstStructure *s = gst_value_get_structure (value); GST_INFO ("validating field %s %" GST_PTR_FORMAT, field, s); validate_rtc_stats (s); gst_structure_get (s, "type", GST_TYPE_WEBRTC_STATS_TYPE, &type, NULL); if (type == GST_WEBRTC_STATS_CODEC) { validate_codec_stats (s); } else if (type == GST_WEBRTC_STATS_INBOUND_RTP) { validate_inbound_rtp_stats (s, stats); state->saw_inbound_rtp = TRUE; } else if (type == GST_WEBRTC_STATS_OUTBOUND_RTP) { validate_outbound_rtp_stats (s, stats); state->saw_outbound_rtp = TRUE; } else if (type == GST_WEBRTC_STATS_REMOTE_INBOUND_RTP) { validate_remote_inbound_rtp_stats (s, stats); state->saw_remote_inbound_rtp = TRUE; } else if (type == GST_WEBRTC_STATS_REMOTE_OUTBOUND_RTP) { validate_remote_outbound_rtp_stats (s, stats); state->saw_remote_outbound_rtp = TRUE; } else if (type == GST_WEBRTC_STATS_CSRC) { } else if (type == GST_WEBRTC_STATS_PEER_CONNECTION) { validate_peer_connection_stats (s); } else if (type == GST_WEBRTC_STATS_DATA_CHANNEL) { } else if (type == GST_WEBRTC_STATS_STREAM) { } else if (type == GST_WEBRTC_STATS_TRANSPORT) { } else if (type == GST_WEBRTC_STATS_CANDIDATE_PAIR) { } else if (type == GST_WEBRTC_STATS_LOCAL_CANDIDATE) { validate_candidate_stats (s, stats); } else if (type == GST_WEBRTC_STATS_REMOTE_CANDIDATE) { validate_candidate_stats (s, stats); } else if (type == GST_WEBRTC_STATS_CERTIFICATE) { } else { g_assert_not_reached (); } return TRUE; } static void validate_stats (struct stats_check_state *state) { gst_structure_foreach_id_str (state->stats, (GstStructureForeachIdStrFunc) validate_stats_foreach, (gpointer) state); } static void _on_stats (GstPromise * promise, gpointer user_data) { struct stats_check_state state = *(struct stats_check_state *) user_data; struct test_webrtc *t = state.t; const GstStructure *reply = gst_promise_get_reply (promise); int i; GST_LOG ("Got stats %" GST_PTR_FORMAT, reply); state.stats = reply; validate_stats (&state); if (state.n_streams > 0 && state.saw_inbound_rtp && state.saw_outbound_rtp && state.saw_remote_inbound_rtp && state.saw_remote_outbound_rtp) { g_mutex_lock (&t->lock); i = GPOINTER_TO_INT (t->user_data); i++; t->user_data = GINT_TO_POINTER (i); g_mutex_unlock (&t->lock); if (i >= 2) test_webrtc_signal_state (t, STATE_CUSTOM); } else { test_webrtc_signal_state (t, STATE_CUSTOM); } gst_promise_unref (promise); } GST_START_TEST (test_session_stats) { struct test_webrtc *t = test_webrtc_new (); GstPromise *p; /* test that the stats generated without any streams are sane */ t->on_negotiation_needed = NULL; test_validate_sdp (t, NULL, NULL); struct stats_check_state state = {.t = t, 0 }; p = gst_promise_new_with_change_func (_on_stats, &state, NULL); g_signal_emit_by_name (t->webrtc1, "get-stats", NULL, p); p = gst_promise_new_with_change_func (_on_stats, &state, NULL); g_signal_emit_by_name (t->webrtc2, "get-stats", NULL, p); test_webrtc_wait_for_state_mask (t, 1 << STATE_CUSTOM); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_stats_with_stream) { struct test_webrtc *t = create_audio_test (); struct stats_check_state state = {.t = t, 0 }; GstPromise *p; /* test that the stats generated with stream are sane */ t->on_offer_created = NULL; t->on_answer_created = NULL; t->on_negotiation_needed = NULL; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); test_webrtc_create_offer (t); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); /* set caps for webrtcbin sink to validate codec stats */ GstCaps *caps = gst_caps_from_string (OPUS_RTP_CAPS (96)); GstPad *pad = gst_element_get_static_pad (t->webrtc1, "sink_0"); gst_pad_set_caps (pad, caps); gst_caps_unref (caps); gst_object_unref (pad); test_webrtc_wait_for_answer_error_eos (t); test_webrtc_signal_state (t, STATE_REMOTE_ANSWER_SET); p = gst_promise_new_with_change_func (_on_stats, &state, NULL); g_signal_emit_by_name (t->webrtc1, "get-stats", NULL, p); p = gst_promise_new_with_change_func (_on_stats, &state, NULL); g_signal_emit_by_name (t->webrtc2, "get-stats", NULL, p); test_webrtc_wait_for_state_mask (t, 1 << STATE_CUSTOM); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_stats_with_two_streams) { /* test that the stats generated with audio and video stream have correct info */ struct test_webrtc *t = test_webrtc_new (); GstPromise *p; t->on_offer_created = NULL; t->on_answer_created = NULL; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; GstHarness *h1 = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_audio_test_src_harness (h1, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h1); GstHarness *h2 = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_video_test_src_harness (h2, 0xCAFECAFE); t->harnesses = g_list_prepend (t->harnesses, h2); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); test_webrtc_create_offer (t); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_webrtc_wait_for_answer_error_eos (t); /* We need to push data until the connection is established and the * statistics start reporting outbound-rtp stats before things will * unblock below */ for (int i = 0; i < 5; i++) { gst_harness_push_from_src (h1); gst_harness_push_from_src (h2); } struct stats_check_state state = {.t = t,.n_streams = 2, 0 }; while (TRUE) { g_usleep (100 * 1000); p = gst_promise_new_with_change_func (_on_stats, &state, NULL); g_signal_emit_by_name (t->webrtc1, "get-stats", NULL, p); p = gst_promise_new_with_change_func (_on_stats, &state, NULL); g_signal_emit_by_name (t->webrtc2, "get-stats", NULL, p); if (test_webrtc_check_for_state_mask (t, 1 << STATE_CUSTOM)) break; gst_harness_push_from_src (h1); gst_harness_push_from_src (h2); } test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_add_transceiver) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction, trans_direction; GstWebRTCRTPTransceiver *trans; direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, NULL, &trans); fail_unless (trans != NULL); g_object_get (trans, "direction", &trans_direction, NULL); fail_unless_equals_int (direction, trans_direction); gst_object_unref (trans); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_get_transceivers) { struct test_webrtc *t = create_audio_test (); GstWebRTCRTPTransceiver *trans; GArray *transceivers; g_signal_emit_by_name (t->webrtc1, "get-transceivers", &transceivers); fail_unless (transceivers != NULL); fail_unless_equals_int (1, transceivers->len); trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 0); fail_unless (trans != NULL); g_array_unref (transceivers); test_webrtc_free (t); } GST_END_TEST; static void on_sdp_media_check_mid (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { const char **mid = user_data; guint i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); gboolean seen_mid = FALSE; guint j; for (j = 0; j < gst_sdp_media_attributes_len (media); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); if (g_strcmp0 (attr->key, "mid") == 0) { fail_unless (!seen_mid); seen_mid = TRUE; fail_unless_equals_string (attr->value, mid[i]); } } } } GST_START_TEST (test_add_recvonly_transceiver) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans; VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); const char *expected_mid[] = { "gst", }; VAL_SDP_INIT (mid, on_sdp_media_check_mid, expected_mid, &count); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &mid); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &mid); const gchar *expected_offer_direction[] = { "recvonly", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GstHarness *h; /* add a transceiver that will only receive an opus stream and check that * the created offer is marked as recvonly */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup recvonly transceiver */ caps = gst_caps_from_string (OPUS_RTP_CAPS (96) ", a-mid=(string)gst"); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); gst_object_unref (trans); /* setup sendonly peer */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_recvonly_sendonly) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans; VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "recvonly", "sendonly" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GstHarness *h; GArray *transceivers; /* add a transceiver that will only receive an opus stream and check that * the created offer is marked as recvonly */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup recvonly transceiver */ caps = gst_caps_from_string (OPUS_RTP_CAPS (96)); gst_caps_set_simple (caps, "ssrc", G_TYPE_UINT, 0xDEADBEEF, NULL); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); gst_object_unref (trans); /* setup sendonly stream */ h = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_fake_audio_src_harness (h, 96, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h); g_signal_emit_by_name (t->webrtc1, "get-transceivers", &transceivers); fail_unless (transceivers != NULL); fail_unless_equals_int (transceivers->len, 2); trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 1); g_object_set (trans, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, NULL); g_array_unref (transceivers); /* setup sendonly peer */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; static void on_sdp_has_datachannel (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { gboolean have_data_channel = FALSE; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { if (_message_media_is_datachannel (desc->sdp, i)) { /* there should only be one data channel m= section */ fail_unless_equals_int (FALSE, have_data_channel); have_data_channel = TRUE; } } fail_unless_equals_int (TRUE, have_data_channel); } GST_START_TEST (test_data_channel_create) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); gchar *label; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); g_object_get (channel, "label", &label, NULL); g_assert_cmpstr (label, ==, "label"); test_validate_sdp (t, &offer, &offer); g_object_unref (channel); g_free (label); test_webrtc_free (t); } GST_END_TEST; static void signal_data_channel (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { test_webrtc_signal_state_unlocked (t, STATE_CUSTOM); } GST_START_TEST (test_data_channel_create_two_channels) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; GObject *channel2 = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); gchar *label; GstStructure *options = NULL; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = signal_data_channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); g_object_get (channel, "label", &label, NULL); g_assert_cmpstr (label, ==, "label"); g_free (label); g_object_unref (channel); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); /* Wait SCTP transport creation */ test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); /* Create another channel on an existing SCTP transport, forcing an ID that should comply with the max-channels requiremennt, this should not raise a critical warning, the id is beneath the required limits. */ options = gst_structure_new ("options", "id", G_TYPE_INT, 2, "negotiated", G_TYPE_BOOLEAN, TRUE, NULL); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label2", options, &channel2); gst_structure_free (options); g_assert_nonnull (channel2); g_object_get (channel2, "label", &label, NULL); g_assert_cmpstr (label, ==, "label2"); g_free (label); g_object_unref (channel2); test_webrtc_free (t); } GST_END_TEST; static void have_data_channel (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { GObject *other = user_data; gchar *our_label, *other_label; g_assert_true (t->error_signal_handler_id > 0); g_object_get (our, "label", &our_label, NULL); g_object_get (other, "label", &other_label, NULL); g_assert_cmpstr (our_label, ==, other_label); g_free (our_label); g_free (other_label); test_webrtc_signal_state_unlocked (t, STATE_CUSTOM); } GST_START_TEST (test_data_channel_remote_notify) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = have_data_channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); t->data_channel_data = channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; static const gchar *test_string = "GStreamer WebRTC is awesome!"; static void on_message_string (GObject * channel, const gchar * str, struct test_webrtc *t) { GstWebRTCDataChannelState state; gchar *expected; g_object_get (channel, "ready-state", &state, NULL); fail_unless_equals_int (GST_WEBRTC_DATA_CHANNEL_STATE_OPEN, state); expected = g_object_steal_data (channel, "expected"); g_assert_cmpstr (expected, ==, str); g_free (expected); test_webrtc_signal_state (t, STATE_CUSTOM); } static void have_data_channel_transfer_string (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { GObject *other = user_data; GstWebRTCDataChannelState state; GError *error = NULL; g_object_get (our, "ready-state", &state, NULL); fail_unless_equals_int (GST_WEBRTC_DATA_CHANNEL_STATE_OPEN, state); g_object_set_data_full (our, "expected", g_strdup (test_string), g_free); fail_unless (gst_webrtc_data_channel_send_string_full (GST_WEBRTC_DATA_CHANNEL (other), test_string, &error)); g_assert_null (error); } GST_START_TEST (test_data_channel_transfer_string) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = have_data_channel_transfer_string; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); t->data_channel_data = channel; g_signal_connect (channel, "on-error", G_CALLBACK (on_channel_error_not_reached), NULL); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; #define g_assert_cmpbytes(b1, b2) \ G_STMT_START { \ gsize l1, l2; \ const guint8 *d1 = g_bytes_get_data (b1, &l1); \ const guint8 *d2 = g_bytes_get_data (b2, &l2); \ g_assert_cmpmem (d1, l1, d2, l2); \ } G_STMT_END; static void on_message_data (GObject * channel, GBytes * data, struct test_webrtc *t) { GstWebRTCDataChannelState state; GBytes *expected; g_object_get (channel, "ready-state", &state, NULL); fail_unless_equals_int (GST_WEBRTC_DATA_CHANNEL_STATE_OPEN, state); expected = g_object_steal_data (channel, "expected"); g_assert_cmpbytes (data, expected); g_bytes_unref (expected); test_webrtc_signal_state (t, STATE_CUSTOM); } static void have_data_channel_transfer_data (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { GObject *other = user_data; GBytes *data = g_bytes_new_static (test_string, strlen (test_string)); GstWebRTCDataChannelState state; GError *error = NULL; g_object_get (our, "ready-state", &state, NULL); fail_unless_equals_int (GST_WEBRTC_DATA_CHANNEL_STATE_OPEN, state); g_object_set_data_full (our, "expected", g_bytes_ref (data), (GDestroyNotify) g_bytes_unref); fail_unless (gst_webrtc_data_channel_send_data_full (GST_WEBRTC_DATA_CHANNEL (other), data, &error)); g_assert_null (error); g_bytes_unref (data); } GST_START_TEST (test_data_channel_transfer_data) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = have_data_channel_transfer_data; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); t->data_channel_data = channel; g_signal_connect (channel, "on-error", G_CALLBACK (on_channel_error_not_reached), NULL); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; static void have_data_channel_create_data_channel (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { GObject *another; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = have_data_channel_transfer_string; g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &another); g_assert_nonnull (another); t->data_channel_data = another; t->data_channel_notify = (GDestroyNotify) g_object_unref; } GST_START_TEST (test_data_channel_create_after_negotiate) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = have_data_channel_create_data_channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "prev-label", NULL, &channel); g_assert_nonnull (channel); t->data_channel_data = channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; struct test_data_channel { GObject *dc1; GObject *dc2; gint n_open; gint n_closed; }; static void have_data_channel_mark_open (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { struct test_data_channel *tdc = t->data_channel_data; tdc->dc2 = g_object_ref (our); if (g_atomic_int_add (&tdc->n_open, 1) == 1) { test_webrtc_signal_state_unlocked (t, STATE_CUSTOM); } } static gboolean is_data_channel_open (GObject * channel) { GstWebRTCDataChannelState ready_state = GST_WEBRTC_DATA_CHANNEL_STATE_CLOSED; if (channel) { g_object_get (channel, "ready-state", &ready_state, NULL); } return ready_state == GST_WEBRTC_DATA_CHANNEL_STATE_OPEN; } static void on_data_channel_open (GObject * channel, GParamSpec * pspec, struct test_webrtc *t) { struct test_data_channel *tdc = t->data_channel_data; if (is_data_channel_open (channel)) { if (g_atomic_int_add (&tdc->n_open, 1) == 1) { test_webrtc_signal_state (t, STATE_CUSTOM); } } } static void on_data_channel_close (GObject * channel, GParamSpec * pspec, struct test_webrtc *t) { struct test_data_channel *tdc = t->data_channel_data; GstWebRTCDataChannelState ready_state; g_object_get (channel, "ready-state", &ready_state, NULL); if (ready_state == GST_WEBRTC_DATA_CHANNEL_STATE_CLOSED) { g_atomic_int_add (&tdc->n_closed, 1); } } GST_START_TEST (test_data_channel_close) { #define NUM_CHANNELS 3 struct test_webrtc *t = test_webrtc_new (); struct test_data_channel tdc = { NULL, }; guint channel_id[NUM_CHANNELS] = { 0, 1, 2 }; gulong sigid = 0; int i; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; t->on_data_channel = have_data_channel_mark_open; t->data_channel_data = &tdc; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); /* open and close NUM_CHANNELS data channels to verify that we can reuse the * stream id of a previously closed data channel and that we have the same * behaviour no matter if we create the channel in READY or PLAYING state */ for (i = 0; i < NUM_CHANNELS; i++) { GWeakRef dc1_ref, dc2_ref; tdc.n_open = 0; tdc.n_closed = 0; g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &tdc.dc1); g_assert_nonnull (tdc.dc1); g_weak_ref_init (&dc1_ref, tdc.dc1); sigid = g_signal_connect (tdc.dc1, "notify::ready-state", G_CALLBACK (on_data_channel_open), t); if (i == 0) { fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); } /* FIXME: Creating a data channel may result in "on-open" being sent * before we even had a chance to register the signal. For this test we * want to make sure that the channel is actually open before we try to * close it. So if we didn't receive the signal we fall back to a 1s * timeout where we explicitly check if both channels are open. */ while (!is_data_channel_open (tdc.dc1) || !is_data_channel_open (tdc.dc2)) g_usleep (100 * 1000); g_object_get (tdc.dc1, "id", &channel_id[i], NULL); g_signal_handler_disconnect (tdc.dc1, sigid); g_weak_ref_init (&dc2_ref, tdc.dc2); g_signal_connect (tdc.dc1, "notify::ready-state", G_CALLBACK (on_data_channel_close), t); g_signal_connect (tdc.dc2, "notify::ready-state", G_CALLBACK (on_data_channel_close), t); test_webrtc_signal_state (t, STATE_NEW); /* currently we assume there is no renegotiation if the last data channel is * removed but if it changes this test could be extended to verify both * the behaviour of removing the last channel as well as the behaviour when * there are still data channels remaining */ t->on_negotiation_needed = _negotiation_not_reached; g_signal_emit_by_name (tdc.dc1, "close"); /* XXX: try to do something better here */ while (g_atomic_int_get (&tdc.n_closed) != 2) g_usleep (100 * 1000); g_clear_object (&tdc.dc1); g_clear_object (&tdc.dc2); /* XXX: try to do something better here */ while (g_weak_ref_get (&dc1_ref) != NULL || g_weak_ref_get (&dc2_ref) != NULL) g_usleep (100 * 1000); g_weak_ref_clear (&dc1_ref); g_weak_ref_clear (&dc2_ref); test_webrtc_signal_state (t, STATE_NEW); test_webrtc_wait_for_state_mask (t, 1 << STATE_NEW); } /* verify the same stream id has been reused for each data channel */ assert_equals_int (channel_id[0], channel_id[1]); assert_equals_int (channel_id[0], channel_id[2]); test_webrtc_free (t); #undef NUM_CHANNELS } GST_END_TEST; static void on_buffered_amount_low_emitted (GObject * channel, struct test_webrtc *t) { test_webrtc_signal_state (t, STATE_CUSTOM); } static void have_data_channel_check_low_threshold_emitted (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { g_signal_connect (our, "on-buffered-amount-low", G_CALLBACK (on_buffered_amount_low_emitted), t); g_object_set (our, "buffered-amount-low-threshold", 1, NULL); g_signal_connect (our, "on-error", G_CALLBACK (on_channel_error_not_reached), NULL); gst_webrtc_data_channel_send_string_full (GST_WEBRTC_DATA_CHANNEL (our), "A", NULL); } GST_START_TEST (test_data_channel_low_threshold) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = NULL; t->on_data_channel = have_data_channel_check_low_threshold_emitted; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); t->data_channel_data = channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; static void have_data_channel_transfer_large_data (struct test_webrtc *t, GstElement * element, GObject * our, gpointer user_data) { GObject *other = user_data; const gsize size = 1024 * 1024; guint8 *random_data = g_new (guint8, size); GBytes *data; gsize i; GError *error = NULL; for (i = 0; i < size; i++) random_data[i] = (guint8) (i & 0xff); data = g_bytes_new_with_free_func (random_data, size, (GDestroyNotify) g_free, random_data); g_object_set_data_full (our, "expected", g_bytes_ref (data), (GDestroyNotify) g_bytes_unref); g_signal_connect (our, "on-message-data", G_CALLBACK (on_message_data), t); g_signal_connect (other, "on-error", G_CALLBACK (on_channel_error_not_reached), NULL); fail_if (gst_webrtc_data_channel_send_data_full (GST_WEBRTC_DATA_CHANNEL (other), data, &error)); g_assert_nonnull (error); g_clear_error (&error); g_bytes_unref (data); test_webrtc_signal_state_unlocked (t, STATE_CUSTOM); } GST_START_TEST (test_data_channel_max_message_size) { struct test_webrtc *t = test_webrtc_new (); GObject *channel = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = NULL; t->on_data_channel = have_data_channel_transfer_large_data; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); g_assert_nonnull (channel); t->data_channel_data = channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 1 << STATE_CUSTOM, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; static void _on_ready_state_notify (GObject * channel, GParamSpec * pspec, struct test_webrtc *t) { gint *n_ready = t->data_channel_data; GstWebRTCDataChannelState ready_state; g_object_get (channel, "ready-state", &ready_state, NULL); if (ready_state == GST_WEBRTC_DATA_CHANNEL_STATE_OPEN) { if (g_atomic_int_add (n_ready, 1) >= 1) { test_webrtc_signal_state (t, STATE_CUSTOM); } } } GST_START_TEST (test_data_channel_pre_negotiated) { struct test_webrtc *t = test_webrtc_new (); GObject *channel1 = NULL, *channel2 = NULL; VAL_SDP_INIT (media_count, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (offer, on_sdp_has_datachannel, NULL, &media_count); GstStructure *s; gint n_ready = 0; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_prepare_data_channel = have_prepare_data_channel; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); s = gst_structure_new ("application/data-channel", "negotiated", G_TYPE_BOOLEAN, TRUE, "id", G_TYPE_INT, 1, NULL); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", s, &channel1); g_assert_nonnull (channel1); g_signal_emit_by_name (t->webrtc2, "create-data-channel", "label", s, &channel2); g_assert_nonnull (channel2); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); test_validate_sdp_full (t, &offer, &offer, 0, FALSE); t->data_channel_data = &n_ready; g_signal_connect (channel1, "notify::ready-state", G_CALLBACK (_on_ready_state_notify), t); g_signal_connect (channel2, "notify::ready-state", G_CALLBACK (_on_ready_state_notify), t); test_webrtc_wait_for_state_mask (t, 1 << STATE_CUSTOM); test_webrtc_signal_state (t, STATE_NEW); have_data_channel_transfer_string (t, t->webrtc1, channel1, channel2); test_webrtc_wait_for_state_mask (t, 1 << STATE_CUSTOM); g_object_unref (channel1); g_object_unref (channel2); gst_structure_free (s); test_webrtc_free (t); } GST_END_TEST; static void _count_non_rejected_media (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * sd, gpointer user_data) { guint expected = GPOINTER_TO_UINT (user_data); guint non_rejected_media; guint i; non_rejected_media = 0; for (i = 0; i < gst_sdp_message_medias_len (sd->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (sd->sdp, i); if (gst_sdp_media_get_port (media) != 0) non_rejected_media += 1; } fail_unless_equals_int (non_rejected_media, expected); } static void _check_bundle_tag (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * sd, gpointer user_data) { gchar **bundled = NULL; GStrv expected = user_data; guint i; fail_unless (_parse_bundle (sd->sdp, &bundled, NULL)); if (!bundled) { fail_unless_equals_int (g_strv_length (expected), 0); } else { fail_unless_equals_int (g_strv_length (bundled), g_strv_length (expected)); } for (i = 0; i < g_strv_length (expected); i++) { fail_unless (g_strv_contains ((const gchar **) bundled, expected[i])); } g_strfreev (bundled); } static void _check_bundle_only_media (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * sd, gpointer user_data) { gchar **expected_bundle_only = user_data; guint i; for (i = 0; i < gst_sdp_message_medias_len (sd->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (sd->sdp, i); const gchar *mid = gst_sdp_media_get_attribute_val (media, "mid"); if (g_strv_contains ((const gchar **) expected_bundle_only, mid)) fail_unless (_media_has_attribute_key (media, "bundle-only")); } } GST_START_TEST (test_bundle_audio_video_max_bundle_max_bundle) { struct test_webrtc *t = create_audio_video_test (); const gchar *bundle[] = { "audio0", "video1", NULL }; const gchar *offer_bundle_only[] = { "video1", NULL }; const gchar *answer_bundle_only[] = { NULL }; guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &payloads); VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (1), &bundle_tag); VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (2), &bundle_tag); VAL_SDP_INIT (offer_bundle, _check_bundle_only_media, &offer_bundle_only, &offer_non_reject); VAL_SDP_INIT (answer_bundle, _check_bundle_only_media, &answer_bundle_only, &answer_non_reject); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_bundle); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_bundle); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); /* We set a max-bundle policy on the offering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, and they should be marked * as bundle-only */ gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); /* We also set a max-bundle policy on the answering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, but need not be marked * as bundle-only. */ gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_audio_video_max_compat_max_bundle) { struct test_webrtc *t = create_audio_video_test (); const gchar *bundle[] = { "audio0", "video1", NULL }; const gchar *bundle_only[] = { NULL }; guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &count); VAL_SDP_INIT (count_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (2), &bundle_tag); VAL_SDP_INIT (bundle_sdp, _check_bundle_only_media, &bundle_only, &count_non_reject); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &bundle_sdp); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &bundle_sdp); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); /* We set a max-compat policy on the offering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, and they should *not* be marked * as bundle-only */ gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-compat"); /* We set a max-bundle policy on the answering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, but need not be marked * as bundle-only. */ gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_audio_video_max_bundle_none) { struct test_webrtc *t = create_audio_video_test (); const gchar *offer_mid[] = { "audio0", "video1", NULL }; const gchar *offer_bundle_only[] = { "video1", NULL }; const gchar *answer_mid[] = { NULL }; const gchar *answer_bundle_only[] = { NULL }; guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); VAL_SDP_INIT (count_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (1), &payloads); VAL_SDP_INIT (offer_bundle_tag, _check_bundle_tag, offer_mid, &count_non_reject); VAL_SDP_INIT (answer_bundle_tag, _check_bundle_tag, answer_mid, &count_non_reject); VAL_SDP_INIT (offer_bundle, _check_bundle_only_media, &offer_bundle_only, &offer_bundle_tag); VAL_SDP_INIT (answer_bundle, _check_bundle_only_media, &answer_bundle_only, &answer_bundle_tag); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_bundle); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_bundle); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); /* We set a max-bundle policy on the offering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, and they should be marked * as bundle-only */ gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); /* We set a none policy on the answering webrtcbin, * this means that the answer should contain no bundled * medias, and as the bundle-policy of the offering webrtcbin * is set to max-bundle, only one media should be active. */ gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "none"); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_audio_video_data) { struct test_webrtc *t = create_audio_video_test (); const gchar *mids[] = { "audio0", "video1", "application2", NULL }; const gchar *offer_bundle_only[] = { "video1", "application2", NULL }; const gchar *answer_bundle_only[] = { NULL }; GObject *channel = NULL; guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (3), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); VAL_SDP_INIT (bundle_tag, _check_bundle_tag, mids, &payloads); VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (1), &bundle_tag); VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (3), &bundle_tag); VAL_SDP_INIT (offer_bundle, _check_bundle_only_media, &offer_bundle_only, &offer_non_reject); VAL_SDP_INIT (answer_bundle, _check_bundle_only_media, &answer_bundle_only, &answer_non_reject); const gchar *expected_offer_setup[] = { "actpass", "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_bundle); const gchar *expected_answer_setup[] = { "active", "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_bundle); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); /* We set a max-bundle policy on the offering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, and they should be marked * as bundle-only */ gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); /* We also set a max-bundle policy on the answering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, but need not be marked * as bundle-only. */ gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); test_validate_sdp (t, &offer, &answer); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_duplicate_nego) { struct test_webrtc *t = create_audio_video_test (); guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstHarness *h; guint negotiation_flag = 0; /* check that negotiating twice succeeds */ t->on_negotiation_needed = on_negotiation_needed_hit; t->negotiation_data = &negotiation_flag; h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); fail_unless (negotiation_flag & (1 << 2)); test_webrtc_reset_negotiation (t); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_dual_audio) { struct test_webrtc *t = create_audio_test (); guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstHarness *h; GstWebRTCRTPTransceiver *trans; GArray *transceivers; guint mline; /* test that each mline gets a unique transceiver even with the same caps */ h = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_fake_audio_src_harness (h, 96, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h); h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); t->on_negotiation_needed = NULL; test_validate_sdp (t, &offer, &answer); g_signal_emit_by_name (t->webrtc1, "get-transceivers", &transceivers); fail_unless (transceivers != NULL); fail_unless_equals_int (2, transceivers->len); trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 0); fail_unless (trans != NULL); g_object_get (trans, "mlineindex", &mline, NULL); fail_unless_equals_int (mline, 0); trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 1); fail_unless (trans != NULL); g_object_get (trans, "mlineindex", &mline, NULL); fail_unless_equals_int (mline, 1); g_array_unref (transceivers); test_webrtc_free (t); } GST_END_TEST; static void sdp_increasing_session_version (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GstWebRTCSessionDescription *previous; const GstSDPOrigin *our_origin, *previous_origin; const gchar *prop; guint64 our_v, previous_v; prop = TEST_SDP_IS_LOCAL (t, element, desc) ? "current-local-description" : "current-remote-description"; g_object_get (element, prop, &previous, NULL); our_origin = gst_sdp_message_get_origin (desc->sdp); previous_origin = gst_sdp_message_get_origin (previous->sdp); our_v = g_ascii_strtoull (our_origin->sess_version, NULL, 10); previous_v = g_ascii_strtoull (previous_origin->sess_version, NULL, 10); ck_assert_int_lt (previous_v, our_v); gst_webrtc_session_description_free (previous); } static void sdp_equal_session_id (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GstWebRTCSessionDescription *previous; const GstSDPOrigin *our_origin, *previous_origin; const gchar *prop; prop = TEST_SDP_IS_LOCAL (t, element, desc) ? "current-local-description" : "current-remote-description"; g_object_get (element, prop, &previous, NULL); our_origin = gst_sdp_message_get_origin (desc->sdp); previous_origin = gst_sdp_message_get_origin (previous->sdp); fail_unless_equals_string (previous_origin->sess_id, our_origin->sess_id); gst_webrtc_session_description_free (previous); } static void sdp_media_equal_attribute (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, GstWebRTCSessionDescription * previous, const gchar * attr) { guint i, n; n = MIN (gst_sdp_message_medias_len (previous->sdp), gst_sdp_message_medias_len (desc->sdp)); for (i = 0; i < n; i++) { const GstSDPMedia *our_media, *other_media; const gchar *our_mid, *other_mid; our_media = gst_sdp_message_get_media (desc->sdp, i); other_media = gst_sdp_message_get_media (previous->sdp, i); our_mid = gst_sdp_media_get_attribute_val (our_media, attr); other_mid = gst_sdp_media_get_attribute_val (other_media, attr); fail_unless_equals_string (our_mid, other_mid); } } static void sdp_media_equal_mid (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GstWebRTCSessionDescription *previous; const gchar *prop; prop = TEST_SDP_IS_LOCAL (t, element, desc) ? "current-local-description" : "current-remote-description"; g_object_get (element, prop, &previous, NULL); sdp_media_equal_attribute (t, element, desc, previous, "mid"); gst_webrtc_session_description_free (previous); } static void sdp_media_equal_ice_params (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GstWebRTCSessionDescription *previous; const gchar *prop; prop = TEST_SDP_IS_LOCAL (t, element, desc) ? "current-local-description" : "current-remote-description"; g_object_get (element, prop, &previous, NULL); sdp_media_equal_attribute (t, element, desc, previous, "ice-ufrag"); sdp_media_equal_attribute (t, element, desc, previous, "ice-pwd"); gst_webrtc_session_description_free (previous); } static void sdp_media_equal_fingerprint (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GstWebRTCSessionDescription *previous; const gchar *prop; prop = TEST_SDP_IS_LOCAL (t, element, desc) ? "current-local-description" : "current-remote-description"; g_object_get (element, prop, &previous, NULL); sdp_media_equal_attribute (t, element, desc, previous, "fingerprint"); gst_webrtc_session_description_free (previous); } GST_START_TEST (test_renego_add_stream) { struct test_webrtc *t = create_audio_video_test (); guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL); VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL, &renego_mid); VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params); VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL, &renego_sess_id); VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL, &renego_sess_ver); GstHarness *h; /* negotiate an AV stream and then renegotiate an extra stream */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL); add_fake_audio_src_harness (h, 98, 0xBEEFFFFF); t->harnesses = g_list_prepend (t->harnesses, h); media_formats.next = &renego_fingerprint; count.user_data = GUINT_TO_POINTER (3); /* renegotiate! */ test_webrtc_reset_negotiation (t); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_renego_stream_add_data_channel) { struct test_webrtc *t = create_audio_video_test (); guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", NULL }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly", NULL }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL); VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL, &renego_mid); VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params); VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL, &renego_sess_id); VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL, &renego_sess_ver); GObject *channel; GstHarness *h; /* negotiate an AV stream and then renegotiate a data channel */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); media_formats.next = &renego_fingerprint; count.user_data = GUINT_TO_POINTER (3); /* renegotiate! */ test_webrtc_reset_negotiation (t); test_validate_sdp (t, &offer, &answer); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_renego_data_channel_add_stream) { struct test_webrtc *t = test_webrtc_new (); guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { NULL, "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { NULL, "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL); VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL, &renego_mid); VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params); VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL, &renego_sess_id); VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL, &renego_sess_ver); GObject *channel; GstHarness *h; /* negotiate an data channel and then renegotiate to add a av stream */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_data_channel = NULL; t->on_pad_added = _pad_added_fakesink; t->on_prepare_data_channel = NULL; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); test_validate_sdp_full (t, &offer, &answer, 0, FALSE); h = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_fake_audio_src_harness (h, 97, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); media_formats.next = &renego_fingerprint; count.user_data = GUINT_TO_POINTER (2); /* renegotiate! */ test_webrtc_reset_negotiation (t); test_validate_sdp_full (t, &offer, &answer, 0, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_renego_stream_data_channel_add_stream) { struct test_webrtc *t = test_webrtc_new (); guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", NULL, "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", NULL, "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL); VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL, &renego_mid); VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params); VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL, &renego_sess_id); VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL, &renego_sess_ver); GObject *channel; GstHarness *h; /* Negotiate a stream and a data channel, then renogotiate with a new stream */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_data_channel = NULL; t->on_prepare_data_channel = NULL; t->on_pad_added = _pad_added_fakesink; h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_fake_audio_src_harness (h, 97, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); g_signal_emit_by_name (t->webrtc1, "create-data-channel", "label", NULL, &channel); test_validate_sdp_full (t, &offer, &answer, 0, FALSE); h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL); add_fake_audio_src_harness (h, 97, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h); media_formats.next = &renego_fingerprint; count.user_data = GUINT_TO_POINTER (3); /* renegotiate! */ test_webrtc_reset_negotiation (t); test_validate_sdp_full (t, &offer, &answer, 0, FALSE); g_object_unref (channel); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_renego_add_stream) { struct test_webrtc *t = create_audio_video_test (); const gchar *bundle[] = { "audio0", "video1", "audio2", NULL }; const gchar *offer_bundle_only[] = { "video1", "audio2", NULL }; const gchar *answer_bundle_only[] = { NULL }; guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, &payloads); VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL, &renego_mid); VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params); VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL, &renego_sess_id); VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL, &renego_sess_ver); VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &renego_fingerprint); VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (1), &bundle_tag); VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (3), &bundle_tag); VAL_SDP_INIT (offer_bundle_only_sdp, _check_bundle_only_media, &offer_bundle_only, &offer_non_reject); VAL_SDP_INIT (answer_bundle_only_sdp, _check_bundle_only_media, &answer_bundle_only, &answer_non_reject); GstHarness *h; /* We set a max-bundle policy on the offering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, and they should be marked * as bundle-only */ gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); /* We also set a max-bundle policy on the answering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, but need not be marked * as bundle-only. */ gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); /* negotiate an AV stream and then renegotiate an extra stream */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL); add_fake_audio_src_harness (h, 98, 0xBEEFFFFF); t->harnesses = g_list_prepend (t->harnesses, h); offer_setup.next = &offer_bundle_only_sdp; answer_setup.next = &answer_bundle_only_sdp; count.user_data = GUINT_TO_POINTER (3); /* renegotiate! */ test_webrtc_reset_negotiation (t); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_max_compat_max_bundle_renego_add_stream) { struct test_webrtc *t = create_audio_video_test (); const gchar *bundle[] = { "audio0", "video1", "audio2", NULL }; const gchar *bundle_only[] = { NULL }; guint media_format_count[] = { 1, 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (renego_mid, sdp_media_equal_mid, NULL, NULL); VAL_SDP_INIT (renego_ice_params, sdp_media_equal_ice_params, NULL, &renego_mid); VAL_SDP_INIT (renego_sess_id, sdp_equal_session_id, NULL, &renego_ice_params); VAL_SDP_INIT (renego_sess_ver, sdp_increasing_session_version, NULL, &renego_sess_id); VAL_SDP_INIT (renego_fingerprint, sdp_media_equal_fingerprint, NULL, &renego_sess_ver); VAL_SDP_INIT (bundle_tag, _check_bundle_tag, bundle, &renego_fingerprint); VAL_SDP_INIT (count_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (3), &bundle_tag); VAL_SDP_INIT (bundle_sdp, _check_bundle_only_media, &bundle_only, &count_non_reject); GstHarness *h; /* We set a max-compat policy on the offering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, and they should *not* be marked * as bundle-only */ gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-compat"); /* We set a max-bundle policy on the answering webrtcbin, * this means that all the offered medias should be part * of the group:BUNDLE attribute, but need not be marked * as bundle-only. */ gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); /* negotiate an AV stream and then renegotiate an extra stream */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); h = gst_harness_new_with_element (t->webrtc1, "sink_2", NULL); add_fake_audio_src_harness (h, 98, 0xBEEFFFFF); t->harnesses = g_list_prepend (t->harnesses, h); media_formats.next = &bundle_sdp; count.user_data = GUINT_TO_POINTER (3); /* renegotiate! */ test_webrtc_reset_negotiation (t); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_renego_transceiver_set_direction) { struct test_webrtc *t = create_audio_test (); guint media_format_count[] = { 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstWebRTCRTPTransceiver *transceiver; GstHarness *h; GstPad *pad; /* negotiate an AV stream and then change the transceiver direction */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); /* renegotiate an inactive transceiver! */ pad = gst_element_get_static_pad (t->webrtc1, "sink_0"); g_object_get (pad, "transceiver", &transceiver, NULL); fail_unless (transceiver != NULL); g_object_set (transceiver, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_INACTIVE, NULL); expected_offer_direction[0] = "inactive"; expected_answer_direction[0] = "inactive"; /* TODO: also validate EOS events from the inactive change */ test_webrtc_reset_negotiation (t); test_validate_sdp (t, &offer, &answer); gst_object_unref (pad); gst_object_unref (transceiver); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_renego_triggering) { struct test_webrtc *t = create_audio_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); const gchar *expected_offer_setup[] = { "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GArray *transceivers; /* Ensure sendrecv stream on webrtc1 */ g_signal_emit_by_name (t->webrtc1, "get-transceivers", &transceivers); fail_unless (transceivers != NULL); fail_unless_equals_int (transceivers->len, 1); GstWebRTCRTPTransceiver *trans_local = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 0); g_object_set (trans_local, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV, NULL); /* setup recvonly peer */ caps = gst_caps_from_string (OPUS_RTP_CAPS (96)); gst_caps_set_simple (caps, "ssrc", G_TYPE_UINT, 0xDEADBEEF, NULL); GstWebRTCRTPTransceiver *trans_remote = NULL; GstWebRTCRTPTransceiverDirection direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc2, "add-transceiver", direction, caps, &trans_remote); gst_caps_unref (caps); fail_unless (trans_remote != NULL); gst_object_unref (trans_remote); test_validate_sdp (t, &offer, &answer); GST_LOG ("Finished validating sendrecv <-> recvonly nego. Triggering renego with recvonly <-> recvonly peers"); /* Now change the sender to recvonly and expect to renegotiate to inactive */ test_webrtc_reset_negotiation (t); test_webrtc_clear_states (t); GST_LOG ("Setting local transceiver to RECVONLY"); g_object_set (trans_local, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, NULL); GST_LOG ("Waiting for on-negotiation-needed"); test_webrtc_wait_for_state_mask (t, 1 << STATE_NEGOTIATION_NEEDED); const gchar *new_expected_offer_direction[] = { "recvonly" }; VAL_SDP_INIT (new_offer, on_sdp_media_direction, new_expected_offer_direction, NULL); const gchar *new_expected_answer_direction[] = { "inactive" }; VAL_SDP_INIT (new_answer, on_sdp_media_direction, new_expected_answer_direction, NULL); test_validate_sdp (t, &new_offer, &new_answer); g_array_unref (transceivers); /* At this point webrtc2 is the answerer. Check that it also triggers nego * if we change the direction to sendonly */ test_webrtc_reset_negotiation (t); test_webrtc_clear_states (t); GST_LOG ("Setting remote transceiver to SENDONLY"); g_signal_emit_by_name (t->webrtc2, "get-transceivers", &transceivers); fail_unless (transceivers != NULL); fail_unless_equals_int (transceivers->len, 1); trans_remote = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 0); g_object_set (trans_remote, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, NULL); g_array_unref (transceivers); GST_LOG ("Waiting for on-negotiation-needed"); test_webrtc_wait_for_state_mask (t, 1 << STATE_NEGOTIATION_NEEDED); test_webrtc_free (t); } GST_END_TEST; static void offer_remove_last_media (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { guint i, n; GstSDPMessage *new, *old; const GstSDPOrigin *origin; const GstSDPConnection *conn; old = t->offer_desc->sdp; fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_new (&new)); origin = gst_sdp_message_get_origin (old); conn = gst_sdp_message_get_connection (old); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_set_version (new, gst_sdp_message_get_version (old))); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_set_origin (new, origin->username, origin->sess_id, origin->sess_version, origin->nettype, origin->addrtype, origin->addr)); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_set_session_name (new, gst_sdp_message_get_session_name (old))); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_set_information (new, gst_sdp_message_get_information (old))); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_set_uri (new, gst_sdp_message_get_uri (old))); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_set_connection (new, conn->nettype, conn->addrtype, conn->address, conn->ttl, conn->addr_number)); n = gst_sdp_message_attributes_len (old); for (i = 0; i < n; i++) { const GstSDPAttribute *a = gst_sdp_message_get_attribute (old, i); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_add_attribute (new, a->key, a->value)); } n = gst_sdp_message_medias_len (old); fail_unless (n > 0); for (i = 0; i < n - 1; i++) { const GstSDPMedia *m = gst_sdp_message_get_media (old, i); GstSDPMedia *new_m; fail_unless_equals_int (GST_SDP_OK, gst_sdp_media_copy (m, &new_m)); fail_unless_equals_int (GST_SDP_OK, gst_sdp_message_add_media (new, new_m)); gst_sdp_media_init (new_m); gst_sdp_media_free (new_m); } gst_webrtc_session_description_free (t->offer_desc); t->offer_desc = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_OFFER, new); } static void offer_set_produced_error (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { const GstStructure *reply; GError *error = NULL; reply = gst_promise_get_reply (promise); fail_unless (gst_structure_get (reply, "error", G_TYPE_ERROR, &error, NULL)); GST_INFO ("error produced: %s", error->message); g_clear_error (&error); test_webrtc_signal_state_unlocked (t, STATE_CUSTOM); } static void offer_created_produced_error (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { const GstStructure *reply; GError *error = NULL; reply = gst_promise_get_reply (promise); fail_unless (gst_structure_get (reply, "error", G_TYPE_ERROR, &error, NULL)); GST_INFO ("error produced: %s", error->message); g_clear_error (&error); } GST_START_TEST (test_renego_lose_media_fails) { struct test_webrtc *t = create_audio_video_test (); VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL); VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (2), NULL); /* check that removing an m=line will produce an error */ test_validate_sdp (t, &offer, &answer); test_webrtc_reset_negotiation (t); t->on_offer_created = offer_remove_last_media; t->on_offer_set = offer_set_produced_error; t->on_answer_created = NULL; test_webrtc_create_offer (t); test_webrtc_wait_for_state_mask (t, 1 << STATE_CUSTOM); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_codec_preferences_rtx_no_duplicate_payloads) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans; guint offer_media_format_count[] = { 2, }; guint answer_media_format_count[] = { 1, }; VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); VAL_SDP_INIT (offer_media_formats, on_sdp_media_count_formats, offer_media_format_count, &payloads); VAL_SDP_INIT (answer_media_formats, on_sdp_media_count_formats, answer_media_format_count, &payloads); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_media_formats); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_media_formats); const gchar *expected_offer_direction[] = { "recvonly", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GstHarness *h; /* add a transceiver that will only receive an opus stream and check that * the created offer is marked as recvonly */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); /* setup recvonly transceiver */ caps = gst_caps_from_string (VP8_RTP_CAPS (96)); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); g_object_set (GST_OBJECT (trans), "do-nack", TRUE, NULL); gst_caps_unref (caps); fail_unless (trans != NULL); gst_object_unref (trans); /* setup sendonly peer */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_video_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; static void on_sdp_media_no_duplicate_extmaps (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, 0); fail_unless (media != NULL); fail_unless_equals_string (gst_sdp_media_get_attribute_val_n (media, "extmap", 0), "1 foobar"); fail_unless (gst_sdp_media_get_attribute_val_n (media, "extmap", 1) == NULL); } /* In this test, we validate that identical extmaps for multiple formats * in the caps of a single transceiver are deduplicated. This is necessary * because Firefox will complain about duplicate extmap ids and fail negotiation * otherwise. */ GST_START_TEST (test_codec_preferences_no_duplicate_extmaps) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiver *trans; GstWebRTCRTPTransceiverDirection direction; VAL_SDP_INIT (extmaps, on_sdp_media_no_duplicate_extmaps, NULL, NULL); GstCaps *caps; GstStructure *s; caps = gst_caps_new_empty (); s = gst_structure_from_string (VP8_RTP_CAPS (96), NULL); gst_structure_set (s, "extmap-1", G_TYPE_STRING, "foobar", NULL); gst_caps_append_structure (caps, s); s = gst_structure_from_string (H264_RTP_CAPS (97), NULL); gst_structure_set (s, "extmap-1", G_TYPE_STRING, "foobar", NULL); gst_caps_append_structure (caps, s); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); t->on_negotiation_needed = NULL; t->on_pad_added = NULL; t->on_ice_candidate = NULL; test_validate_sdp (t, &extmaps, NULL); test_webrtc_free (t); } GST_END_TEST; /* In this test, we validate that trying to use different values * for the same extmap id in multiple formats in the caps of a * single transceiver errors out when creating the offer. */ GST_START_TEST (test_codec_preferences_incompatible_extmaps) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiver *trans; GstWebRTCRTPTransceiverDirection direction; GstCaps *caps; GstStructure *s; caps = gst_caps_new_empty (); s = gst_structure_from_string (VP8_RTP_CAPS (96), NULL); gst_structure_set (s, "extmap-1", G_TYPE_STRING, "foobar", NULL); gst_caps_append_structure (caps, s); s = gst_structure_from_string (H264_RTP_CAPS (97), NULL); gst_structure_set (s, "extmap-1", G_TYPE_STRING, "foobaz", NULL); gst_caps_append_structure (caps, s); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); t->on_negotiation_needed = NULL; t->on_pad_added = NULL; t->on_ice_candidate = NULL; t->on_offer_created = offer_created_produced_error; test_validate_sdp_full (t, NULL, NULL, 1 << STATE_ERROR, TRUE); test_webrtc_free (t); } GST_END_TEST; /* In this test, we validate that extmap values must be of the correct type */ GST_START_TEST (test_codec_preferences_invalid_extmap) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiver *trans; GstWebRTCRTPTransceiverDirection direction; GstCaps *caps; GstStructure *s; caps = gst_caps_new_empty (); s = gst_structure_from_string (VP8_RTP_CAPS (96), NULL); gst_structure_set (s, "extmap-1", G_TYPE_INT, 42, NULL); gst_caps_append_structure (caps, s); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); t->on_negotiation_needed = NULL; t->on_pad_added = NULL; t->on_ice_candidate = NULL; t->on_offer_created = offer_created_produced_error; test_validate_sdp_full (t, NULL, NULL, 1 << STATE_ERROR, TRUE); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_reject_request_pad) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans, *trans2; guint offer_media_format_count[] = { 1, }; guint answer_media_format_count[] = { 1, }; VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); VAL_SDP_INIT (offer_media_formats, on_sdp_media_count_formats, offer_media_format_count, &payloads); VAL_SDP_INIT (answer_media_formats, on_sdp_media_count_formats, answer_media_format_count, &payloads); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_media_formats); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_media_formats); const gchar *expected_offer_direction[] = { "recvonly", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GstHarness *h; GstPad *pad; GstPadTemplate *templ; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); /* setup recvonly transceiver */ caps = gst_caps_from_string (VP8_RTP_CAPS (96)); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_video_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); /* This should fail because the direction is wrong */ pad = gst_element_request_pad_simple (t->webrtc1, "sink_0"); fail_unless (pad == NULL); g_object_set (trans, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV, NULL); templ = gst_element_get_pad_template (t->webrtc1, "sink_%u"); fail_unless (templ != NULL); /* This should fail because the caps are wrong */ caps = gst_caps_from_string (OPUS_RTP_CAPS (96)); pad = gst_element_request_pad (t->webrtc1, templ, "sink_0", caps); fail_unless (pad == NULL); g_object_set (trans, "codec-preferences", NULL, NULL); /* This should fail because the kind doesn't match */ pad = gst_element_request_pad (t->webrtc1, templ, "sink_0", caps); fail_unless (pad == NULL); gst_caps_unref (caps); /* This should succeed and give us sink_0 */ pad = gst_element_request_pad_simple (t->webrtc1, "sink_0"); fail_unless (pad != NULL); g_object_get (pad, "transceiver", &trans2, NULL); fail_unless (trans == trans2); gst_object_unref (pad); gst_object_unref (trans); gst_object_unref (trans2); test_webrtc_free (t); } GST_END_TEST; static void _verify_media_types (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { gchar **media_types = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); fail_unless_equals_string (gst_sdp_media_get_media (media), media_types[i]); } } GST_START_TEST (test_reject_create_offer) { struct test_webrtc *t = test_webrtc_new (); GstHarness *h; GstPromise *promise; GstPromiseResult res; const GstStructure *s; GError *error = NULL; const gchar *media_types[] = { "video", "audio" }; VAL_SDP_INIT (media_type, _verify_media_types, &media_types, NULL); guint media_format_count[] = { 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &media_type); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass" }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", "active" }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv" }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly" }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup sendonly peer */ h = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); /* Check that if there is no 0, we can't create an offer with a hole */ promise = gst_promise_new (); g_signal_emit_by_name (t->webrtc1, "create-offer", NULL, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_structure_get (s, "error", G_TYPE_ERROR, &error, NULL); fail_unless (g_error_matches (error, GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE)); fail_unless_matches_string (error->message, "Tranceiver with mid \\(null\\) has locked mline 1 but the offer only has 0 sections"); g_clear_error (&error); gst_promise_unref (promise); h = gst_harness_new_with_element (t->webrtc1, "sink_%u", NULL); add_fake_video_src_harness (h, 97, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h); /* Adding a second sink, which will fill m-line 0, should fix it */ test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_reject_create_offer_mline_locked_no_caps) { GstHarness *h; GstPromise *promise; const GstStructure *s; GstPromiseResult res; GError *error = NULL; h = gst_harness_new_with_padnames ("webrtcbin", "sink_0", NULL); promise = gst_promise_new (); g_signal_emit_by_name (h->element, "create-offer", NULL, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_structure_get (s, "error", G_TYPE_ERROR, &error, NULL); fail_unless (g_error_matches (error, GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE)); fail_unless_equals_string (error->message, "Trying to add transceiver at line 0 but there is a transceiver with a" " locked mline for this line which doesn't have caps"); g_clear_error (&error); gst_promise_unref (promise); gst_harness_teardown (h); } GST_END_TEST; GST_START_TEST (test_reject_set_description) { struct test_webrtc *t = test_webrtc_new (); GstHarness *h; GstPromise *promise; GstPromiseResult res; const GstStructure *s; GError *error = NULL; GstWebRTCSessionDescription *desc = NULL; GstPadTemplate *templ; GstCaps *caps; GstPad *pad; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup peer 1 */ h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); /* Create a second side with specific video caps */ templ = gst_element_get_pad_template (t->webrtc2, "sink_%u"); fail_unless (templ != NULL); caps = gst_caps_from_string (VP8_RTP_CAPS (97)); pad = gst_element_request_pad (t->webrtc2, templ, "sink_0", caps); fail_unless (pad != NULL); gst_caps_unref (caps); gst_object_unref (pad); /* Create an offer */ promise = gst_promise_new (); g_signal_emit_by_name (t->webrtc1, "create-offer", NULL, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_structure_get (s, "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &desc, NULL); fail_unless (desc != NULL); gst_promise_unref (promise); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); /* Verify that setting an offer where there is a forced m-line with a different kind fails. */ promise = gst_promise_new (); g_signal_emit_by_name (t->webrtc2, "set-remote-description", desc, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); gst_structure_get (s, "error", G_TYPE_ERROR, &error, NULL); fail_unless (g_error_matches (error, GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE)); fail_unless_matches_string (error->message, "m-line 0 with transceiver was locked to video, but SDP has audio media"); g_clear_error (&error); fail_unless (s != NULL); gst_promise_unref (promise); gst_webrtc_session_description_free (desc); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_force_second_media) { struct test_webrtc *t = test_webrtc_new (); const gchar *media_types[] = { "audio" }; VAL_SDP_INIT (media_type, _verify_media_types, &media_types, NULL); guint media_format_count[] = { 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &media_type); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &media_formats); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &media_formats); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer_direction, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer_direction, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (answer_count, _count_num_sdp_media, GUINT_TO_POINTER (1), &answer_direction); VAL_SDP_INIT (offer_count, _count_num_sdp_media, GUINT_TO_POINTER (1), &offer_direction); const gchar *second_media_types[] = { "audio", "video" }; VAL_SDP_INIT (second_media_type, _verify_media_types, &second_media_types, NULL); guint second_media_format_count[] = { 1, 1 }; VAL_SDP_INIT (second_media_formats, on_sdp_media_count_formats, second_media_format_count, &second_media_type); const gchar *second_expected_offer_setup[] = { "active", "actpass" }; VAL_SDP_INIT (second_offer_setup, on_sdp_media_setup, second_expected_offer_setup, &second_media_formats); const gchar *second_expected_answer_setup[] = { "passive", "active" }; VAL_SDP_INIT (second_answer_setup, on_sdp_media_setup, second_expected_answer_setup, &second_media_formats); const gchar *second_expected_answer_direction[] = { "sendonly", "recvonly" }; VAL_SDP_INIT (second_answer_direction, on_sdp_media_direction, second_expected_answer_direction, &second_answer_setup); const gchar *second_expected_offer_direction[] = { "recvonly", "sendrecv" }; VAL_SDP_INIT (second_offer_direction, on_sdp_media_direction, second_expected_offer_direction, &second_offer_setup); VAL_SDP_INIT (second_answer_count, _count_num_sdp_media, GUINT_TO_POINTER (2), &second_answer_direction); VAL_SDP_INIT (second_offer_count, _count_num_sdp_media, GUINT_TO_POINTER (2), &second_offer_direction); GstHarness *h; guint negotiation_flag = 0; GstPadTemplate *templ; GstCaps *caps; GstPad *pad; /* add a transceiver that will only receive an opus stream and check that * the created offer is marked as recvonly */ t->on_negotiation_needed = on_negotiation_needed_hit; t->negotiation_data = &negotiation_flag; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup peer */ h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); /* Create a second side with specific video caps */ templ = gst_element_get_pad_template (t->webrtc2, "sink_%u"); fail_unless (templ != NULL); caps = gst_caps_from_string (VP8_RTP_CAPS (97)); pad = gst_element_request_pad (t->webrtc2, templ, NULL, caps); gst_caps_unref (caps); fail_unless (pad != NULL); h = gst_harness_new_with_element (t->webrtc2, GST_PAD_NAME (pad), NULL); gst_object_unref (pad); add_fake_video_src_harness (h, 97, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer_count, &answer_count); fail_unless (negotiation_flag & 1 << 2); test_webrtc_reset_negotiation (t); t->offerror = 2; test_validate_sdp (t, &second_offer_count, &second_answer_count); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_codec_preferences_caps) { GstHarness *h; GstPad *pad; GstWebRTCRTPTransceiver *trans; GstCaps *caps, *caps2; h = gst_harness_new_with_padnames ("webrtcbin", "sink_0", NULL); pad = gst_element_get_static_pad (h->element, "sink_0"); g_object_get (pad, "transceiver", &trans, NULL); caps = gst_caps_from_string ("application/x-rtp, media=video," "encoding-name=VP8, payload=115; application/x-rtp, media=video," " encoding-name=H264, payload=104"); g_object_set (trans, "codec-preferences", caps, NULL); caps2 = gst_pad_query_caps (pad, NULL); fail_unless (gst_caps_is_equal (caps, caps2)); gst_caps_unref (caps2); gst_caps_unref (caps); caps = gst_caps_from_string (VP8_RTP_CAPS (115)); fail_unless (gst_pad_query_accept_caps (pad, caps)); gst_harness_set_src_caps (h, g_steal_pointer (&caps)); caps = gst_caps_from_string (VP8_RTP_CAPS (99)); fail_unless (!gst_pad_query_accept_caps (pad, caps)); gst_caps_unref (caps); gst_object_unref (pad); gst_object_unref (trans); gst_harness_teardown (h); } GST_END_TEST; GST_START_TEST (test_codec_preferences_negotiation_sinkpad) { struct test_webrtc *t = test_webrtc_new (); guint media_format_count[] = { 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); VAL_SDP_INIT (payloads2, on_sdp_media_payload_types, GUINT_TO_POINTER (0), &count); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &payloads2); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstPad *pad; GstWebRTCRTPTransceiver *transceiver; GstHarness *h; GstCaps *caps; GstPromise *promise; GstPromiseResult res; const GstStructure *s; GError *error = NULL; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); pad = gst_element_get_static_pad (t->webrtc1, "sink_0"); g_object_get (pad, "transceiver", &transceiver, NULL); caps = gst_caps_from_string (VP8_RTP_CAPS (115) ";" VP8_RTP_CAPS (97)); g_object_set (transceiver, "codec-preferences", caps, NULL); gst_caps_unref (caps); gst_object_unref (transceiver); gst_object_unref (pad); add_fake_video_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); promise = gst_promise_new (); g_signal_emit_by_name (t->webrtc1, "create-offer", NULL, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_structure_get (s, "error", G_TYPE_ERROR, &error, NULL); fail_unless (g_error_matches (error, GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE)); fail_unless_equals_string ("Caps negotiation on pad sink_0 failed against codec preferences", error->message); g_clear_error (&error); gst_promise_unref (promise); caps = gst_caps_from_string (VP8_RTP_CAPS (97)); gst_harness_set_src_caps (h, caps); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; static void add_audio_test_src_harness (GstHarness * h, guint ssrc) { #define L16_CAPS "application/x-rtp, payload=11, media=audio," \ " encoding-name=L16, clock-rate=44100, ssrc=(uint)3484078952" GstCaps *caps = gst_caps_from_string (L16_CAPS); GstElement *capsfilter; if (ssrc != 0) { gst_caps_set_simple (caps, "ssrc", G_TYPE_UINT, ssrc, NULL); } gst_harness_add_src_parse (h, "audiotestsrc is-live=true ! rtpL16pay ! " "capsfilter name=capsfilter ! identity", TRUE); capsfilter = gst_bin_get_by_name (GST_BIN (h->src_harness->element), "capsfilter"); g_object_set (G_OBJECT (capsfilter), "caps", caps, NULL); gst_harness_set_src_caps (h, caps); caps = NULL; gst_clear_object (&capsfilter); #undef L16_CAPS } static void add_video_test_src_harness (GstHarness * h, guint ssrc) { GstCaps *caps = gst_caps_from_string (VP8_RTP_CAPS (96)); GstElement *capsfilter; if (ssrc != 0) { gst_caps_set_simple (caps, "ssrc", G_TYPE_UINT, ssrc, NULL); } gst_harness_add_src_parse (h, "videotestsrc is-live=true ! video/x-raw,width=16,height=16 ! vp8enc ! rtpvp8pay ! " "capsfilter name=capsfilter ! identity", TRUE); capsfilter = gst_bin_get_by_name (GST_BIN (h->src_harness->element), "capsfilter"); g_object_set (G_OBJECT (capsfilter), "caps", caps, NULL); gst_harness_set_src_caps (h, caps); caps = NULL; gst_clear_object (&capsfilter); } struct pad_added_harness_data { GList *sink_harnesses; OnPadAdded on_pad_added; gpointer on_pad_added_data; }; static void _pad_added_harness (struct test_webrtc *t, GstElement * element, GstPad * pad, gpointer user_data) { struct pad_added_harness_data *data = user_data; GstHarness *h; if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC) return; h = gst_harness_new_with_element (element, NULL, GST_OBJECT_NAME (pad)); t->harnesses = g_list_prepend (t->harnesses, h); if (data) { data->sink_harnesses = g_list_prepend (data->sink_harnesses, h); g_cond_broadcast (&t->cond); if (data->on_pad_added) data->on_pad_added (t, element, pad, data->on_pad_added_data); } } GST_START_TEST (test_audio_sendrecv) { struct test_webrtc *t = test_webrtc_new (); GstHarness *h1, *h2; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; h1 = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_audio_test_src_harness (h1, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h1); h2 = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_audio_test_src_harness (h2, 0xBEEFDEAD); t->harnesses = g_list_prepend (t->harnesses, h2); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendrecv", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstWebRTCKind expected_kind = GST_WEBRTC_KIND_AUDIO; g_signal_connect (t->webrtc1, "on-new-transceiver", G_CALLBACK (on_new_transceiver_expected_kind), GUINT_TO_POINTER (expected_kind)); g_signal_connect (t->webrtc2, "on-new-transceiver", G_CALLBACK (on_new_transceiver_expected_kind), GUINT_TO_POINTER (expected_kind)); test_validate_sdp (t, &offer, &answer); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); /* Exchange a few buffers between webrtcbin1 and webrtcbin2 to check that they can handle incoming data and we get no errors on the bus. */ for (int i = 0; i < 5; i++) { gst_harness_push_from_src (h1); gst_harness_push_from_src (h2); } test_webrtc_free (t); } GST_END_TEST; static void on_sdp_media_rtp_header_extensions (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GArray *expected_extensions = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); if (g_strcmp0 (gst_sdp_media_get_media (media), "audio") == 0 || g_strcmp0 (gst_sdp_media_get_media (media), "video") == 0) { int extension_idx = 0; int j; for (j = 0; j < gst_sdp_media_attributes_len (media); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); if (g_strcmp0 (attr->key, "extmap") == 0) { GStrv split = g_strsplit (attr->value, " ", 2); fail_unless_equals_string (split[1], g_array_index (expected_extensions, char *, extension_idx++)); g_strfreev (split); } } fail_unless_equals_int (expected_extensions->len, extension_idx); } } } #define TWCC_URI "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" GST_START_TEST (test_rtp_header_extension_sendonly_recvonly_pair) { struct test_webrtc *t = test_webrtc_new (); GstHarness *h; GstWebRTCRTPTransceiver *trans; GstCaps *caps; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); caps = gst_caps_from_string (OPUS_RTP_CAPS (96) ", extmap-1=(string)" TWCC_URI); GstStructure *s = gst_caps_get_structure (caps, 0); gst_structure_set (s, "ssrc", G_TYPE_UINT, 0xDEADBEEF, NULL); gst_structure_set (s, "payload", G_TYPE_INT, 96, NULL); gst_harness_set_src_caps (h, gst_caps_copy (caps)); gst_harness_add_src_parse (h, "fakesrc is-live=true", TRUE); t->harnesses = g_list_prepend (t->harnesses, h); g_signal_emit_by_name (t->webrtc1, "get-transceiver", 0, &trans); g_object_set (trans, "direction", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, NULL); gst_object_unref (trans); g_signal_emit_by_name (t->webrtc2, "add-transceiver", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, caps, &trans); fail_unless (trans != NULL); gst_object_unref (trans); gst_caps_unref (caps); const gchar *expected_offer_direction[] = { "sendonly", }; VAL_SDP_INIT (offer_direction, on_sdp_media_direction, expected_offer_direction, NULL); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer_direction, on_sdp_media_direction, expected_answer_direction, NULL); const gchar *EXPECTED_EXTENSIONS_DATA[] = { TWCC_URI, }; GArray *expected_extensions = g_array_new (FALSE, FALSE, sizeof (gchar *)); g_array_append_vals (expected_extensions, EXPECTED_EXTENSIONS_DATA, sizeof (EXPECTED_EXTENSIONS_DATA) / sizeof (gchar *)); VAL_SDP_INIT (offer, on_sdp_media_rtp_header_extensions, expected_extensions, &offer_direction); VAL_SDP_INIT (answer, on_sdp_media_rtp_header_extensions, expected_extensions, &answer_direction); test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); g_array_free (expected_extensions, TRUE); } GST_END_TEST; GST_START_TEST (test_invalid_bundle_in_pending_remote_description) { GstPromise *promise; struct test_webrtc *t = test_webrtc_new (); const gchar *invalid_bundle = "v=0\r\n\ o=thisisadapterortc 2683876491 2 IN IP4 127.0.0.1\r\n\ s=-\r\n\ t=0 0\r\n\ a=setup:actpass\r\n\ a=fingerprint:sha-256 95:B3:DB:24:83:3B:9E:3F:B0:AD:93:2D:EF:73:C9:D2:1C:68:EA:19:C6:F8:73:BA:9A:FA:34:A9:64:69:C0:D8\r\n\ a=ice-ufrag:ERn4TYI2HSbtKrzQNCdp9wD2EHt4wM2O\r\n\ a=ice-pwd:QxEZFuCPRwIURJPMSYNNYFr2XFNgqNkG\r\n\ a=group:BUNDLE \r\n\ "; GstSDPMessage *sdp; const GstStructure *reply; GError *error = NULL; t->on_negotiation_needed = NULL; t->on_offer_created = NULL; t->on_answer_created = NULL; gst_sdp_message_new_from_text (invalid_bundle, &sdp); GstWebRTCSessionDescription *desc = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_OFFER, sdp); gst_element_set_state (t->webrtc1, GST_STATE_READY); promise = gst_promise_new (); g_signal_emit_by_name (t->webrtc1, "set-remote-description", desc, promise); gst_promise_wait (promise); gst_promise_unref (promise); gst_webrtc_session_description_free (desc); /* Creating an answer from SDP with invalid BUNDLE group should trigger no crash. */ promise = gst_promise_new (); g_signal_emit_by_name (t->webrtc1, "create-answer", NULL, promise); gst_promise_wait (promise); reply = gst_promise_get_reply (promise); fail_unless (gst_structure_get (reply, "error", G_TYPE_ERROR, &error, NULL)); fail_unless (g_error_matches (error, GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_SDP_SYNTAX_ERROR)); fail_unless_equals_string (error->message, "Invalid format for BUNDLE group, expected at least one mid (BUNDLE )"); g_clear_error (&error); gst_promise_unref (promise); test_webrtc_free (t); } GST_END_TEST; static void new_jitterbuffer_set_fast_start (GstElement * rtpbin, GstElement * rtpjitterbuffer, guint session_id, guint ssrc, gpointer user_data) { g_object_set (rtpjitterbuffer, "faststart-min-packets", 1, NULL); } GST_START_TEST (test_codec_preferences_negotiation_srcpad) { struct test_webrtc *t = test_webrtc_new (); guint media_format_count[] = { 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &count); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &payloads); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &payloads); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (0), &count); GstHarness *h; struct pad_added_harness_data pad_added_data = { NULL, }; GstHarness *sink_harness = NULL; guint i; GstElement *rtpbin2; GstBuffer *buf; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_harness; t->pad_added_data = &pad_added_data; rtpbin2 = gst_bin_get_by_name (GST_BIN (t->webrtc2), "rtpbin"); fail_unless (rtpbin2 != NULL); g_signal_connect (rtpbin2, "new-jitterbuffer", G_CALLBACK (new_jitterbuffer_set_fast_start), NULL); g_object_unref (rtpbin2); h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_audio_test_src_harness (h, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); for (i = 0; i < 10; i++) gst_harness_push_from_src (h); g_mutex_lock (&t->lock); while (pad_added_data.sink_harnesses == NULL) { gst_harness_push_from_src (h); g_cond_wait_until (&t->cond, &t->lock, g_get_monotonic_time () + 5000); } fail_unless_equals_int (1, g_list_length (pad_added_data.sink_harnesses)); sink_harness = (GstHarness *) pad_added_data.sink_harnesses->data; g_mutex_unlock (&t->lock); fail_unless (sink_harness->element == t->webrtc2); /* Get one buffer out, this makes sure the capsfilter is primed and * avoids races. */ buf = gst_harness_pull (sink_harness); fail_unless (buf != NULL); gst_buffer_unref (buf); gst_harness_set_sink_caps_str (sink_harness, OPUS_RTP_CAPS (100)); test_webrtc_reset_negotiation (t); test_validate_sdp_full (t, &offer, &answer_non_reject, 0, FALSE); /* check that the mid/mline is correct */ { GstWebRTCRTPTransceiver *rtp_trans; GstPad *srcpad; guint mline; srcpad = gst_pad_get_peer (sink_harness->sinkpad); fail_unless (srcpad != NULL); g_object_get (srcpad, "transceiver", &rtp_trans, NULL); gst_clear_object (&srcpad); fail_unless (rtp_trans != NULL); g_object_get (rtp_trans, "mlineindex", &mline, NULL); gst_clear_object (&rtp_trans); fail_unless_equals_int (mline, 0); } test_webrtc_free (t); g_list_free (pad_added_data.sink_harnesses); } GST_END_TEST; static void _on_new_transceiver_codec_preferences_h264 (GstElement * webrtcbin, GstWebRTCRTPTransceiver * trans, gpointer * user_data) { GstCaps *caps; caps = gst_caps_from_string ("application/x-rtp,encoding-name=(string)H264"); g_object_set (trans, "codec-preferences", caps, NULL); gst_caps_unref (caps); } static void on_sdp_media_payload_types_only_h264 (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { const GstSDPMedia *vmedia; guint video_mline = GPOINTER_TO_UINT (user_data); guint j; vmedia = gst_sdp_message_get_media (desc->sdp, video_mline); for (j = 0; j < gst_sdp_media_attributes_len (vmedia); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (vmedia, j); if (!g_strcmp0 (attr->key, "rtpmap")) { fail_unless_equals_string (attr->value, "101 H264/90000"); } } } GST_START_TEST (test_codec_preferences_in_on_new_transceiver) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans; VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint offer_media_format_count[] = { 2 }; guint answer_media_format_count[] = { 1 }; VAL_SDP_INIT (offer_media_formats, on_sdp_media_count_formats, offer_media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (answer_media_formats, on_sdp_media_count_formats, answer_media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (offer_count, _count_num_sdp_media, GUINT_TO_POINTER (1), &offer_media_formats); VAL_SDP_INIT (answer_count, _count_num_sdp_media, GUINT_TO_POINTER (1), &answer_media_formats); VAL_SDP_INIT (offer_payloads, on_sdp_media_payload_types, GUINT_TO_POINTER (0), &offer_count); VAL_SDP_INIT (answer_payloads, on_sdp_media_payload_types_only_h264, GUINT_TO_POINTER (0), &answer_count); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_payloads); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_payloads); const gchar *expected_offer_direction[] = { "sendonly", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GstHarness *h; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup sendonly transceiver with VP8 and H264 */ caps = gst_caps_from_string (VP8_RTP_CAPS (97) ";" H264_RTP_CAPS (101)); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); gst_object_unref (trans); /* setup recvonly peer */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_video_src_harness (h, 101, 0); t->harnesses = g_list_prepend (t->harnesses, h); /* connect to "on-new-transceiver" to set codec-preferences to H264 */ g_signal_connect (t->webrtc2, "on-new-transceiver", G_CALLBACK (_on_new_transceiver_codec_preferences_h264), NULL); /* Answer SDP should now have H264 only. Without the codec-preferences it * would only have VP8 because that comes first in the SDP */ test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_renego_rtx) { struct test_webrtc *t = create_audio_video_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count_media, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); VAL_SDP_INIT (payloads, on_sdp_media_payload_types, GUINT_TO_POINTER (1), &count_media); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", }; VAL_SDP_INIT (offer_direction, on_sdp_media_direction, expected_offer_direction, &payloads); const gchar *expected_answer_direction[] = { "recvonly", "recvonly", }; VAL_SDP_INIT (answer_direction, on_sdp_media_direction, expected_answer_direction, &payloads); const gchar *expected_offer_setup[] = { "actpass", "actpass", }; VAL_SDP_INIT (offer, on_sdp_media_setup, expected_offer_setup, &offer_direction); const gchar *expected_answer_setup[] = { "active", "active", }; VAL_SDP_INIT (answer, on_sdp_media_setup, expected_answer_setup, &answer_direction); GstWebRTCRTPTransceiver *trans; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; test_validate_sdp (t, &offer, &answer); test_webrtc_reset_negotiation (t); g_signal_emit_by_name (t->webrtc1, "get-transceiver", 1, &trans); g_object_set (trans, "do-nack", TRUE, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, NULL); g_clear_object (&trans); g_signal_emit_by_name (t->webrtc2, "get-transceiver", 1, &trans); g_object_set (trans, "do-nack", TRUE, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, NULL); g_clear_object (&trans); /* adding RTX/RED/FEC increases the number of media formats */ media_format_count[1] = 5; test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_bundle_mid_header_extension) { struct test_webrtc *t = test_webrtc_new (); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans; VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); const char *expected_mid[] = { "gst", }; VAL_SDP_INIT (mid, on_sdp_media_check_mid, expected_mid, &count); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &mid); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &mid); const gchar *expected_offer_direction[] = { "recvonly", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "sendonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstCaps *caps; GstHarness *h; guint mline; char *trans_mid; /* add a transceiver that will only receive an opus stream and check that * the created offer is marked as recvonly */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_fakesink; /* setup recvonly transceiver */ caps = gst_caps_from_string (OPUS_RTP_CAPS (96) ", a-mid=(string)gst"); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); gst_caps_unref (caps); fail_unless (trans != NULL); g_object_get (trans, "mlineindex", &mline, NULL); fail_unless_equals_int (mline, -1); /* setup sendonly peer */ h = gst_harness_new_with_element (t->webrtc2, "sink_0", NULL); add_fake_audio_src_harness (h, 96, 0xDEADBEEF); t->harnesses = g_list_prepend (t->harnesses, h); test_validate_sdp (t, &offer, &answer); g_object_get (trans, "mlineindex", &mline, "mid", &trans_mid, NULL); fail_unless_equals_int (mline, 0); fail_unless_equals_string (trans_mid, "gst"); g_clear_pointer (&trans_mid, g_free); gst_object_unref (trans); test_webrtc_free (t); } GST_END_TEST; static void on_new_transceiver_set_rtx_fec (GstElement * webrtcbin, GObject * trans, gpointer user_data) { g_object_set (trans, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, "fec-percentage", 100, "do-nack", TRUE, NULL); } struct pad_properties { const char *mid; guint mlineindex; }; struct new_pad_validate_properties { guint n_props; struct pad_properties *props; }; static void on_pad_added_validate_props (struct test_webrtc *t, GstElement * element, GstPad * pad, gpointer user_data) { GstWebRTCRTPTransceiver *rtp_trans; struct new_pad_validate_properties *pad_props = user_data; char *trans_mid; guint mlineindex; int i; g_object_get (pad, "transceiver", &rtp_trans, NULL); fail_unless (rtp_trans); g_object_get (rtp_trans, "mid", &trans_mid, "mlineindex", &mlineindex, NULL); fail_unless (trans_mid != NULL); fail_unless (mlineindex != -1); for (i = 0; i < pad_props->n_props; i++) { struct pad_properties *expected = &pad_props->props[i]; if (g_strcmp0 (expected->mid, trans_mid) == 0) { if (expected->mlineindex != -1) { fail_unless_equals_int (mlineindex, expected->mlineindex); break; } } } if (i == pad_props->n_props) fail ("could not find a matching expected output pad for mid %s and mline %u", trans_mid, mlineindex); g_clear_pointer (&trans_mid, g_free); gst_clear_object (&rtp_trans); } GST_START_TEST (test_max_bundle_fec) { struct test_webrtc *t = test_webrtc_new (); guint media_format_count[] = { 5, 5, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &media_formats); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &payloads); VAL_SDP_INIT (offer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (1), &count); VAL_SDP_INIT (answer_non_reject, _count_non_rejected_media, GUINT_TO_POINTER (2), &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_non_reject); const gchar *expected_answer_setup[] = { "active", "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &answer_non_reject); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstHarness *src0, *src1; struct pad_properties pad_prop[] = { {"audio0", 0}, {"audio1", 1}, }; struct new_pad_validate_properties validate_pad = { 2, pad_prop }; struct pad_added_harness_data pad_added_data = { NULL, on_pad_added_validate_props, &validate_pad }; guint i; GstElement *rtpbin2; GstBuffer *buf; guint ssrcs[] = { 123456789, 987654321 }; GArray *ssrcs_received; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_harness; t->pad_added_data = &pad_added_data; gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); rtpbin2 = gst_bin_get_by_name (GST_BIN (t->webrtc2), "rtpbin"); fail_unless (rtpbin2 != NULL); g_signal_connect (rtpbin2, "new-jitterbuffer", G_CALLBACK (new_jitterbuffer_set_fast_start), NULL); g_object_unref (rtpbin2); g_signal_connect (t->webrtc1, "on-new-transceiver", G_CALLBACK (on_new_transceiver_set_rtx_fec), NULL); g_signal_connect (t->webrtc2, "on-new-transceiver", G_CALLBACK (on_new_transceiver_set_rtx_fec), NULL); src0 = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_audio_test_src_harness (src0, ssrcs[0]); t->harnesses = g_list_prepend (t->harnesses, src0); src1 = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_audio_test_src_harness (src1, ssrcs[1]); t->harnesses = g_list_prepend (t->harnesses, src1); test_validate_sdp (t, &offer, &answer); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); for (i = 0; i < 10; i++) { gst_harness_push_from_src (src0); gst_harness_push_from_src (src1); } ssrcs_received = g_array_new (FALSE, TRUE, sizeof (guint32)); /* Get one buffer out for each ssrc sent. */ g_mutex_lock (&t->lock); while (ssrcs_received->len < G_N_ELEMENTS (ssrcs)) { GList *sink_harnesses = pad_added_data.sink_harnesses; GList *l; guint i; gst_harness_push_from_src (src0); gst_harness_push_from_src (src1); if (g_list_length (sink_harnesses) < 2) { g_cond_wait_until (&t->cond, &t->lock, g_get_monotonic_time () + 5000); if (g_list_length (sink_harnesses) < 2) continue; } g_mutex_unlock (&t->lock); for (l = sink_harnesses; l; l = l->next) { GstHarness *sink_harness = (GstHarness *) l->data; GstRTPBuffer rtp = GST_RTP_BUFFER_INIT; guint ssrc; fail_unless (sink_harness->element == t->webrtc2); buf = gst_harness_try_pull (sink_harness); if (!buf) continue; fail_unless (gst_rtp_buffer_map (buf, GST_MAP_READ, &rtp)); ssrc = gst_rtp_buffer_get_ssrc (&rtp); for (i = 0; i < ssrcs_received->len; i++) { if (g_array_index (ssrcs_received, guint, i) == ssrc) break; } if (i == ssrcs_received->len) { g_array_append_val (ssrcs_received, ssrc); } gst_rtp_buffer_unmap (&rtp); gst_buffer_unref (buf); } g_mutex_lock (&t->lock); } g_mutex_unlock (&t->lock); GST_DEBUG_BIN_TO_DOT_FILE (GST_BIN (t->webrtc1), GST_DEBUG_GRAPH_SHOW_ALL, "webrtc1-fec-final"); GST_DEBUG_BIN_TO_DOT_FILE (GST_BIN (t->webrtc2), GST_DEBUG_GRAPH_SHOW_ALL, "webrtc2-fec-final"); test_webrtc_free (t); g_list_free (pad_added_data.sink_harnesses); g_array_unref (ssrcs_received); } GST_END_TEST; #define RTPHDREXT_MID GST_RTP_HDREXT_BASE "sdes:mid" #define RTPHDREXT_STREAM_ID GST_RTP_HDREXT_BASE "sdes:rtp-stream-id" #define RTPHDREXT_REPAIRED_STREAM_ID GST_RTP_HDREXT_BASE "sdes:repaired-rtp-stream-id" #define L16_CAPS "application/x-rtp, payload=11, media=audio," \ " encoding-name=L16, clock-rate=44100" static GstCaps * create_simulcast_audio_caps (GstWebRTCRTPTransceiverDirection direction, guint n_rid, guint ssrc[], const char *mid, guint mid_ext_id, const char *const *rids, guint stream_ext_id, guint repaired_ext_id) { GstStructure *s; GstCaps *caps; const char *dir_str; if (direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY) dir_str = "recv"; else if (direction == GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY) dir_str = "send"; else g_assert_not_reached (); caps = gst_caps_from_string (L16_CAPS); s = gst_caps_get_structure (caps, 0); if (mid && mid_ext_id != G_MAXUINT) { char *extmap_key = g_strdup_printf ("extmap-%u", mid_ext_id); gst_structure_set (s, "a-mid", G_TYPE_STRING, mid, extmap_key, G_TYPE_STRING, RTPHDREXT_MID, NULL); g_free (extmap_key); } if (rids && n_rid > 0 && stream_ext_id != G_MAXUINT) { GString *simulcast_value = g_string_new (dir_str); char *extmap_key, *value; int i; g_string_append_c (simulcast_value, ' '); for (i = 0; i < n_rid; i++) { char *rid_key = g_strdup_printf ("rid-%s", rids[i]); gst_structure_set (s, rid_key, G_TYPE_STRING, dir_str, NULL); if (i > 0) g_string_append_c (simulcast_value, ';'); g_string_append (simulcast_value, rids[i]); g_free (rid_key); } value = g_string_free (simulcast_value, FALSE); simulcast_value = NULL; extmap_key = g_strdup_printf ("extmap-%u", stream_ext_id); gst_structure_set (s, extmap_key, G_TYPE_STRING, RTPHDREXT_STREAM_ID, "a-simulcast", G_TYPE_STRING, value, NULL); g_clear_pointer (&extmap_key, g_free); g_clear_pointer (&value, g_free); if (repaired_ext_id != G_MAXUINT) { extmap_key = g_strdup_printf ("extmap-%u", repaired_ext_id); gst_structure_set (s, extmap_key, G_TYPE_STRING, RTPHDREXT_REPAIRED_STREAM_ID, NULL); g_clear_pointer (&extmap_key, g_free); } } return caps; } static void add_simulcast_audio_test_src_harness (GstHarness * h, guint n_rid, guint ssrc[], const char *mid, guint mid_ext_id, const char *const *rids, guint stream_ext_id, guint repaired_ext_id) { GstRTPHeaderExtension *ext; GstElement *capsfilter; char *launch_str; GstCaps *caps; int i; caps = create_simulcast_audio_caps (GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, n_rid, ssrc, mid, mid_ext_id, rids, stream_ext_id, repaired_ext_id); gst_harness_set_src_caps (h, gst_caps_ref (caps)); if (n_rid == 0) { launch_str = g_strdup ("audiotestsrc is-live=true ! " L16_CAPS ",ssrc=(uint)3384078954 ! rtpL16pay name=payloader0"); } else { GString *launch = g_string_new (NULL); for (i = 0; i < n_rid; i++) { const char *rtpfunnel = "funnel."; if (i == 0) rtpfunnel = "rtpfunnel name=funnel ! capsfilter name=capsfilter"; g_string_append_printf (launch, "audiotestsrc is-live=true ! " "rtpL16pay name=payloader%u ! " L16_CAPS ", ssrc=(uint)%u ! %s ", i, ssrc[i], rtpfunnel); } launch_str = g_string_free (launch, FALSE); } GST_INFO ("generated launch string %s", launch_str); gst_harness_add_src_parse (h, launch_str, TRUE); g_clear_pointer (&launch_str, g_free); capsfilter = gst_bin_get_by_name (GST_BIN (h->src_harness->element), "capsfilter"); g_object_set (capsfilter, "caps", caps, NULL); gst_clear_object (&capsfilter); gst_clear_caps (&caps); for (i = 0; i == 0 || i < n_rid; i++) { const char *rid = n_rid > 0 ? rids[i] : NULL; char *pay_name = g_strdup_printf ("payloader%u", i); GstElement *payloader = gst_bin_get_by_name (GST_BIN (h->src_harness->element), pay_name); fail_unless (payloader); g_clear_pointer (&pay_name, g_free); if (mid_ext_id != G_MAXUINT) { ext = gst_rtp_header_extension_create_from_uri (RTPHDREXT_MID); fail_unless (ext); gst_rtp_header_extension_set_id (ext, mid_ext_id); g_object_set (ext, "mid", mid, NULL); g_signal_emit_by_name (payloader, "add-extension", ext); gst_clear_object (&ext); } if (n_rid > 0 && stream_ext_id != G_MAXUINT) { ext = gst_rtp_header_extension_create_from_uri (RTPHDREXT_STREAM_ID); fail_unless (ext); gst_rtp_header_extension_set_id (ext, stream_ext_id); g_object_set (ext, "rid", rid, NULL); g_signal_emit_by_name (payloader, "add-extension", ext); gst_clear_object (&ext); } if (n_rid > 0 && repaired_ext_id != G_MAXUINT) { ext = gst_rtp_header_extension_create_from_uri (RTPHDREXT_REPAIRED_STREAM_ID); fail_unless (ext); gst_rtp_header_extension_set_id (ext, repaired_ext_id); g_object_set (ext, "rid", rid, NULL); g_signal_emit_by_name (payloader, "add-extension", ext); gst_clear_object (&ext); } gst_clear_object (&payloader); } } #undef L16_CAPS static gboolean gst_g_ptr_array_find_str (GPtrArray * ptr, const char *needle, guint * index) { guint i; for (i = 0; i < ptr->len; i++) { const char *test = g_ptr_array_index (ptr, i); if (g_strcmp0 (test, needle) == 0) { if (index) *index = i; return TRUE; } } return FALSE; } struct ExpectedRid { guint n_rid; const char *const *rid; }; static void on_sdp_media_rid (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { struct ExpectedRid *expected_rids = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); struct ExpectedRid *expected_rid = &expected_rids[i]; GPtrArray *seen_rid = g_ptr_array_new_with_free_func (g_free); int j; for (j = 0; j < gst_sdp_media_attributes_len (media); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); if (g_strcmp0 (attr->key, "rid") == 0) { const char *p; char *v; guint k; p = attr->value; /* take up to either space or nul-terminator */ while (p && *p && *p == ' ') p++; v = (char *) p; /* take up to either space or nul-terminator */ while (p && *p && *p != ' ') p++; g_assert (v != p); v = g_strndup (v, p - v); GST_INFO ("rid = %s", v); fail_unless (FALSE == gst_g_ptr_array_find_str (seen_rid, v, NULL), "duplicate/multiple rid for media %u", i); for (k = 0; k < expected_rid->n_rid; k++) { GST_LOG ("expected %u = %s", k, expected_rid->rid[k]); if (g_strcmp0 (v, expected_rid->rid[k]) == 0) break; } fail_unless (k < expected_rid->n_rid, "rid %s not found in media %u", v, i); g_ptr_array_add (seen_rid, v); } } fail_unless (seen_rid->len == expected_rid->n_rid, "mismatch in number of rid's in media %u, seen %u, expected %u", i, seen_rid->len, expected_rid->n_rid); g_ptr_array_unref (seen_rid); } } static void do_test_simulcast (gboolean enable_fec_rtx) { struct test_webrtc *t = test_webrtc_new (); guint media_format_count[] = { enable_fec_rtx ? 5 : 1, }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, NULL); VAL_SDP_INIT (payloads, on_sdp_media_no_duplicate_payloads, NULL, &media_formats); const char *expected_rids0[] = { "a", "z" }; struct ExpectedRid expected_rids = { G_N_ELEMENTS (expected_rids0), expected_rids0 }; VAL_SDP_INIT (rids, on_sdp_media_rid, &expected_rids, &payloads); VAL_SDP_INIT (non_reject, _count_non_rejected_media, GUINT_TO_POINTER (1), &rids); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &non_reject); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_answer_setup[] = { "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_direction[] = { "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstHarness *h; GObject *trans; guint i; GstElement *rtpbin2; GstBuffer *buf; guint mid_ext_id = 1; guint stream_ext_id = 2; guint repaired_ext_id = 3; const char *mid = "5"; guint ssrcs[] = { 123456789, 987654321 }; GArray *ssrcs_received; GstCaps *caps; struct pad_properties pad_prop = { mid, 0 }; struct new_pad_validate_properties validate_pad = { 1, &pad_prop }; struct pad_added_harness_data pad_added_data = { NULL, on_pad_added_validate_props, &validate_pad }; t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; t->on_pad_added = _pad_added_harness; t->pad_added_data = &pad_added_data; gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); if (enable_fec_rtx) { g_signal_connect (t->webrtc1, "on-new-transceiver", G_CALLBACK (on_new_transceiver_set_rtx_fec), NULL); g_signal_connect (t->webrtc2, "on-new-transceiver", G_CALLBACK (on_new_transceiver_set_rtx_fec), NULL); } rtpbin2 = gst_bin_get_by_name (GST_BIN (t->webrtc2), "rtpbin"); fail_unless (rtpbin2 != NULL); g_signal_connect (rtpbin2, "new-jitterbuffer", G_CALLBACK (new_jitterbuffer_set_fast_start), NULL); g_object_unref (rtpbin2); h = gst_harness_new_with_element (t->webrtc1, "sink_0", NULL); add_simulcast_audio_test_src_harness (h, expected_rids.n_rid, ssrcs, mid, mid_ext_id, expected_rids.rid, stream_ext_id, repaired_ext_id); t->harnesses = g_list_prepend (t->harnesses, h); /* setup recvonly transceiver as answer */ caps = create_simulcast_audio_caps (GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, expected_rids.n_rid, ssrcs, mid, mid_ext_id, expected_rids.rid, stream_ext_id, repaired_ext_id); g_signal_emit_by_name (t->webrtc2, "add-transceiver", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, caps, &trans); gst_clear_caps (&caps); fail_unless (trans != NULL); g_clear_object (&trans); test_validate_sdp (t, &offer, &answer); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); for (i = 0; i < 10; i++) { gst_harness_push_from_src (h); } ssrcs_received = g_array_new (FALSE, TRUE, sizeof (guint32)); /* Get one buffer out for each ssrc sent. */ g_mutex_lock (&t->lock); while (ssrcs_received->len < G_N_ELEMENTS (ssrcs)) { GList *sink_harnesses = pad_added_data.sink_harnesses; GList *l; guint i; gst_harness_push_from_src (h); if (g_list_length (pad_added_data.sink_harnesses) < 2) { g_cond_wait_until (&t->cond, &t->lock, g_get_monotonic_time () + 5000); if (g_list_length (pad_added_data.sink_harnesses) < 2) continue; } g_mutex_unlock (&t->lock); for (l = sink_harnesses; l; l = l->next) { GstHarness *sink_harness = (GstHarness *) l->data; GstRTPBuffer rtp = GST_RTP_BUFFER_INIT; guint ssrc; fail_unless (sink_harness->element == t->webrtc2); buf = gst_harness_try_pull (sink_harness); if (!buf) continue; fail_unless (gst_rtp_buffer_map (buf, GST_MAP_READ, &rtp)); ssrc = gst_rtp_buffer_get_ssrc (&rtp); for (i = 0; i < ssrcs_received->len; i++) { if (g_array_index (ssrcs_received, guint, i) == ssrc) break; } if (i == ssrcs_received->len) { g_array_append_val (ssrcs_received, ssrc); } gst_rtp_buffer_unmap (&rtp); gst_buffer_unref (buf); } g_mutex_lock (&t->lock); } g_mutex_unlock (&t->lock); test_webrtc_free (t); g_list_free (pad_added_data.sink_harnesses); g_array_unref (ssrcs_received); } GST_START_TEST (test_simulcast) { do_test_simulcast (FALSE); } GST_END_TEST; GST_START_TEST (test_simulcast_fec_rtx) { do_test_simulcast (TRUE); } GST_END_TEST; GST_START_TEST (test_bundle_multiple_media_rtx_payload_mapping) { struct test_webrtc *t = test_webrtc_new (); guint offer_media_format_count[] = { 5, 5, }; VAL_SDP_INIT (payloads0, on_sdp_media_payload_types, GUINT_TO_POINTER (0), NULL); VAL_SDP_INIT (payloads1, on_sdp_media_payload_types, GUINT_TO_POINTER (1), &payloads0); VAL_SDP_INIT (no_dup_payloads, on_sdp_media_no_duplicate_payloads, NULL, &payloads1); VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, offer_media_format_count, &no_dup_payloads); const gchar *expected_offer_setup[] = { "actpass", "actpass", }; VAL_SDP_INIT (setup, on_sdp_media_setup, expected_offer_setup, &media_formats); const gchar *expected_offer_direction[] = { "recvonly", "recvonly", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &setup); GstWebRTCRTPTransceiverDirection direction; GstWebRTCRTPTransceiver *trans; GstCaps *caps; /* add two identical transceivers that will only receive a vp8 stream and check that * the created offer has the same rtx/red mappings */ t->on_negotiation_needed = NULL; t->on_ice_candidate = NULL; gst_util_set_object_arg (G_OBJECT (t->webrtc1), "bundle-policy", "max-bundle"); gst_util_set_object_arg (G_OBJECT (t->webrtc2), "bundle-policy", "max-bundle"); /* setup recvonly transceiver */ caps = gst_caps_from_string (VP8_RTP_CAPS (97)); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); fail_unless (trans != NULL); g_object_set (GST_OBJECT (trans), "do-nack", TRUE, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, NULL); gst_object_unref (trans); direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY; g_signal_emit_by_name (t->webrtc1, "add-transceiver", direction, caps, &trans); fail_unless (trans != NULL); g_object_set (GST_OBJECT (trans), "do-nack", TRUE, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, NULL); gst_object_unref (trans); gst_caps_unref (caps); /* don't really care about the answer */ test_validate_sdp (t, &offer, NULL); test_webrtc_free (t); } GST_END_TEST; static void add_media_line (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { GstSDPMedia *media = NULL; const GstSDPMedia *existing_media; GstSDPResult res; existing_media = gst_sdp_message_get_media (desc->sdp, 0); res = gst_sdp_media_copy (existing_media, &media); fail_unless (res == GST_SDP_OK); res = gst_sdp_message_add_media (desc->sdp, media); fail_unless (res == GST_SDP_OK); gst_sdp_media_free (media); } static void on_answer_set_rejected (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { const GstStructure *s; GError *error = NULL; GError *compare_error = user_data; s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_structure_get (s, "error", G_TYPE_ERROR, &error, NULL); fail_unless (g_error_matches (error, compare_error->domain, compare_error->code)); fail_unless_equals_string (compare_error->message, error->message); g_clear_error (&error); } GST_START_TEST (test_invalid_add_media_in_answer) { struct test_webrtc *t = create_audio_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (1), &media_formats); const gchar *expected_offer_setup[] = { "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &count); const gchar *expected_offer_direction[] = { "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); VAL_SDP_INIT (answer, add_media_line, NULL, NULL); GError answer_set_error = { GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_SDP_SYNTAX_ERROR, (gchar *) "Answer doesn't have the same number of m-lines as the offer." }; /* Ensure that if the answer has more m-lines than the offer, it gets * rejected. */ t->on_answer_set = on_answer_set_rejected; t->answer_set_data = &answer_set_error; test_validate_sdp (t, &offer, &answer); test_webrtc_free (t); } GST_END_TEST; #define VALID_TURN_SERVER_URL1 "turn://testuser:testpass@test.com:1234" #define VALID_TURN_SERVER_URL2 "turns://1665056262%3Atestuser:T4VwcehYgPAa5bpFAO14gVE19so=@test.com:1234" #define INVALID_TURN_SERVER_URL1 "testuser@testpass@test.com:1234" /* protocol of uri is missing */ #define INVALID_TURN_SERVER_URL2 "turns://testuser:testpass/@test.com:1234" /* unescaped character in password */ #define INVALID_TURN_SERVER_URL3 "turns://test.com:1234" /* 'user:pass' is missing */ GST_START_TEST (test_add_turn_server) { struct test_webrtc *t = test_webrtc_new (); gboolean ret = FALSE; g_signal_emit_by_name (t->webrtc1, "add-turn-server", VALID_TURN_SERVER_URL1, &ret); fail_unless (ret != FALSE); g_signal_emit_by_name (t->webrtc1, "add-turn-server", VALID_TURN_SERVER_URL2, &ret); fail_unless (ret != FALSE); g_signal_emit_by_name (t->webrtc1, "add-turn-server", INVALID_TURN_SERVER_URL1, &ret); fail_unless (ret != TRUE); g_signal_emit_by_name (t->webrtc1, "add-turn-server", INVALID_TURN_SERVER_URL2, &ret); fail_unless (ret != TRUE); g_signal_emit_by_name (t->webrtc1, "add-turn-server", INVALID_TURN_SERVER_URL3, &ret); fail_unless (ret != TRUE); test_webrtc_free (t); } GST_END_TEST; GST_START_TEST (test_data_channel_recreate_offer) { GstHarness *h; GstWebRTCDataChannel *channel; GstPromise *promise; const GstStructure *s; GstPromiseResult res; GstPad *pad; h = gst_harness_new_with_padnames ("webrtcbin", "sink_0", NULL); add_audio_test_src_harness (h, 0xDEADBEEF); g_signal_emit_by_name (h->element, "create-data-channel", "label", NULL, &channel); fail_unless (GST_IS_WEBRTC_DATA_CHANNEL (channel)); pad = gst_element_get_static_pad (h->element, "sink_0"); fail_unless (pad != NULL); promise = gst_promise_new (); g_signal_emit_by_name (h->element, "create-offer", NULL, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_promise_unref (promise); promise = gst_promise_new (); g_signal_emit_by_name (h->element, "create-offer", NULL, promise); res = gst_promise_wait (promise); fail_unless_equals_int (res, GST_PROMISE_RESULT_REPLIED); s = gst_promise_get_reply (promise); fail_unless (s != NULL); gst_promise_unref (promise); gst_object_unref (pad); gst_object_unref (channel); gst_harness_teardown (h); } GST_END_TEST; static void validate_msid (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { char **expected_msid = user_data; int i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); gboolean have_msid = FALSE; char *prev_msid = NULL; int j; for (j = 0; j < gst_sdp_media_attributes_len (media); j++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); const char *start; if (!attr->value) continue; start = strstr (attr->value, "msid:"); if (start) { const char *end; char *msid; start += strlen ("msid:"); end = strstr (start, " "); msid = g_strndup (start, end - start); fail_unless (end, "Invalid msid attribute"); fail_if (have_msid && g_strcmp0 (prev_msid, msid) != 0, "different values for multiple msid values at mline %u, " "prev msid %s, msid %s", i, prev_msid, msid); have_msid = TRUE; fail_unless_equals_string (msid, expected_msid[i]); g_clear_pointer (&prev_msid, g_free); prev_msid = msid; } } g_clear_pointer (&prev_msid, g_free); fail_unless (have_msid, "no msid attribute in media %u", i); } } static void _pad_added_src_check_msid (struct test_webrtc *t, GstElement * element, GstPad * pad, gpointer user_data) { const char *expected_msid = user_data; char *msid; if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC) return; g_object_get (pad, "msid", &msid, NULL); fail_unless_equals_string (msid, expected_msid); g_clear_pointer (&msid, g_free); test_webrtc_signal_state_unlocked (t, STATE_CUSTOM); } GST_START_TEST (test_msid) { struct test_webrtc *t = create_audio_test (); VAL_SDP_INIT (no_duplicate_payloads, on_sdp_media_no_duplicate_payloads, NULL, NULL); guint media_format_count[] = { 1, 5 }; VAL_SDP_INIT (media_formats, on_sdp_media_count_formats, media_format_count, &no_duplicate_payloads); VAL_SDP_INIT (count, _count_num_sdp_media, GUINT_TO_POINTER (2), &media_formats); const gchar *expected_offer_msid[] = { "a1", "a1", }; VAL_SDP_INIT (offer_msid, validate_msid, expected_offer_msid, &count); const gchar *expected_offer_setup[] = { "actpass", "actpass", }; VAL_SDP_INIT (offer_setup, on_sdp_media_setup, expected_offer_setup, &offer_msid); const gchar *expected_offer_direction[] = { "sendrecv", "sendrecv", }; VAL_SDP_INIT (offer, on_sdp_media_direction, expected_offer_direction, &offer_setup); const gchar *expected_answer_setup[] = { "active", "active", }; VAL_SDP_INIT (answer_setup, on_sdp_media_setup, expected_answer_setup, &count); const gchar *expected_answer_direction[] = { "recvonly", "recvonly", }; VAL_SDP_INIT (answer, on_sdp_media_direction, expected_answer_direction, &answer_setup); GstPad *pad; GstHarness *src; GstElement *rtpbin2; t->on_pad_added = _pad_added_src_check_msid; t->pad_added_data = (gpointer) "a1"; rtpbin2 = gst_bin_get_by_name (GST_BIN (t->webrtc2), "rtpbin"); fail_unless (rtpbin2 != NULL); g_signal_connect (rtpbin2, "new-jitterbuffer", G_CALLBACK (new_jitterbuffer_set_fast_start), NULL); g_object_unref (rtpbin2); g_signal_connect (t->webrtc1, "on-new-transceiver", G_CALLBACK (on_new_transceiver_set_rtx_fec), NULL); g_signal_connect (t->webrtc2, "on-new-transceiver", G_CALLBACK (on_new_transceiver_set_rtx_fec), NULL); src = gst_harness_new_with_element (t->webrtc1, "sink_1", NULL); add_audio_test_src_harness (src, 0x12345678); t->harnesses = g_list_prepend (t->harnesses, src); pad = gst_element_get_static_pad (t->webrtc1, "sink_0"); g_object_set (pad, "msid", "a1", NULL); gst_clear_object (&pad); pad = gst_element_get_static_pad (t->webrtc1, "sink_1"); g_object_set (pad, "msid", "a1", NULL); gst_clear_object (&pad); test_validate_sdp (t, &offer, &answer); fail_if (gst_element_set_state (t->webrtc1, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE); while (TRUE) { gst_harness_push_from_src (src); if (test_webrtc_check_for_state_mask (t, 1 << STATE_CUSTOM)) break; g_usleep (10 * 1000); } test_webrtc_free (t); } GST_END_TEST; static void _check_ice_end_of_candidates (struct test_webrtc *t, GstElement * element, guint mlineindex, gchar * candidate, GstElement * other, gpointer user_data) { gint *end_count = user_data; if (!candidate || candidate[0] == '\0') { g_atomic_int_inc (end_count); } } static void sdp_media_has_end_of_candidates (struct test_webrtc *t, GstElement * element, GstWebRTCSessionDescription * desc, gpointer user_data) { guint i; for (i = 0; i < gst_sdp_message_medias_len (desc->sdp); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (desc->sdp, i); fail_unless_equals_string (gst_sdp_media_get_attribute_val_n (media, "end-of-candidates", 0), ""); fail_unless (gst_sdp_media_get_attribute_val_n (media, "end-of-candidates", 1) == NULL); } } GST_START_TEST (test_ice_end_of_candidates) { struct test_webrtc *t = create_audio_test (); GstWebRTCSessionDescription *local_desc; gint end_candidate_count = 0; VAL_SDP_INIT (offer, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); VAL_SDP_INIT (answer, _count_num_sdp_media, GUINT_TO_POINTER (1), NULL); t->on_ice_candidate = _check_ice_end_of_candidates; t->ice_candidate_data = &end_candidate_count; test_validate_sdp (t, &offer, &answer); test_webrtc_wait_for_ice_gathering_complete (t); fail_unless_equals_int (end_candidate_count, 2); g_object_get (t->webrtc1, "current-local-description", &local_desc, NULL); sdp_media_has_end_of_candidates (t, t->webrtc1, local_desc, NULL); gst_webrtc_session_description_free (local_desc); test_webrtc_free (t); } GST_END_TEST; static void _set_setup_session_attr_on_answer (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { GstSDPMessage *sdp; GstSDPMessage *modified_sdp = NULL; const GstSDPMedia *media; GstSDPMedia *modified_media; const gchar *attr; if (TEST_IS_OFFER_ELEMENT (t, element)) return; sdp = t->answer_desc->sdp; media = gst_sdp_message_get_media (sdp, 0); attr = gst_sdp_media_get_attribute_val (media, "setup"); /* Remove the setup attribute from first media */ gst_sdp_media_copy (media, &modified_media); for (unsigned index = 0; index < gst_sdp_media_attributes_len (modified_media); index++) { const GstSDPAttribute *current = gst_sdp_media_get_attribute (modified_media, index); if (!g_str_equal (current->key, "setup")) continue; gst_sdp_media_remove_attribute (modified_media, index); break; } gst_sdp_message_copy (sdp, &modified_sdp); /* Add session-level setup attribute to modified answer */ gst_sdp_message_add_attribute (modified_sdp, "setup", attr); /* Replace first media of answer with a media without session attribute */ gst_sdp_message_remove_media (modified_sdp, 0); gst_sdp_message_add_media (modified_sdp, modified_media); gst_sdp_media_free (modified_media); gst_sdp_message_free (sdp); t->answer_desc->sdp = modified_sdp; } static void _offer_created_do_nothing (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { } GST_START_TEST (test_sdp_session_setup_attribute) { struct test_webrtc *t = create_audio_test (); t->on_offer_created = _offer_created_do_nothing; t->on_answer_created = _set_setup_session_attr_on_answer; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); test_webrtc_create_offer (t); test_webrtc_wait_for_state_mask (t, 1 << STATE_REMOTE_ANSWER_SET); test_webrtc_wait_for_ice_gathering_complete (t); test_webrtc_free (t); } GST_END_TEST; static void _rollback_complete (GstPromise * promise, gpointer user_data) { struct test_webrtc *t = user_data; GstPromiseResult result = gst_promise_wait (promise); fail_unless (result == GST_PROMISE_RESULT_REPLIED); const GstStructure *reply = gst_promise_get_reply (promise); GError *error = NULL; if (reply != NULL && gst_structure_get (reply, "error", G_TYPE_ERROR, &error, NULL)) { /* Ignore invalid-state error, which just means WebRTCbin already processed the remote * offer/answer and went back to stable state */ if (error->domain != GST_WEBRTC_ERROR || error->code != GST_WEBRTC_ERROR_INVALID_STATE) { fail ("rollback request resulted in error: %s", error->message); g_clear_error (&error); return; } g_clear_error (&error); } g_mutex_lock (&t->lock); gint i = GPOINTER_TO_INT (t->user_data); i++; t->user_data = GINT_TO_POINTER (i); g_mutex_unlock (&t->lock); GST_INFO ("%d rollbacks complete", i); /* Signal completion once 2 rollbacks are done */ if (i >= 2) test_webrtc_signal_state (t, STATE_CUSTOM); } static void _rollback_offer (struct test_webrtc *t, GstElement * element, GstPromise * promise, gpointer user_data) { GstWebRTCSessionDescription *rollback = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ROLLBACK, NULL); promise = gst_promise_new_with_change_func (_rollback_complete, t, NULL); if (element == t->webrtc1) { g_signal_emit_by_name (t->webrtc1, "set-local-description", rollback, promise); } else { g_signal_emit_by_name (t->webrtc2, "set-remote-description", rollback, promise); } gst_promise_unref (promise); gst_webrtc_session_description_free (rollback); } GST_START_TEST (test_offer_rollback) { struct test_webrtc *t = create_audio_test (); t->on_offer_created = NULL; t->on_answer_created = NULL; t->on_offer_set = _rollback_offer; t->offer_set_data = NULL; t->perform_create_answer = FALSE; t->on_answer_set = NULL; t->answer_set_data = NULL; fail_if (gst_element_set_state (t->webrtc1, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); fail_if (gst_element_set_state (t->webrtc2, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE); test_webrtc_create_offer (t); test_webrtc_wait_for_state_mask (t, 1 << STATE_CUSTOM); test_webrtc_free (t); } GST_END_TEST; static Suite * webrtcbin_suite (void) { Suite *s = suite_create ("webrtcbin"); TCase *tc = tcase_create ("general"); GstPluginFeature *nicesrc, *nicesink, *dtlssrtpdec, *dtlssrtpenc; GstPluginFeature *sctpenc, *sctpdec, *vp8enc; GstRegistry *registry; registry = gst_registry_get (); nicesrc = gst_registry_lookup_feature (registry, "nicesrc"); nicesink = gst_registry_lookup_feature (registry, "nicesink"); dtlssrtpenc = gst_registry_lookup_feature (registry, "dtlssrtpenc"); dtlssrtpdec = gst_registry_lookup_feature (registry, "dtlssrtpdec"); sctpenc = gst_registry_lookup_feature (registry, "sctpenc"); sctpdec = gst_registry_lookup_feature (registry, "sctpdec"); vp8enc = gst_registry_lookup_feature (registry, "vp8enc"); tcase_add_test (tc, test_no_nice_elements_request_pad); tcase_add_test (tc, test_no_nice_elements_state_change); if (nicesrc && nicesink && dtlssrtpenc && dtlssrtpdec) { tcase_add_test (tc, test_sdp_no_media); tcase_add_test (tc, test_session_stats); tcase_add_test (tc, test_stats_with_stream); if (vp8enc) { tcase_add_test (tc, test_stats_with_two_streams); } else { GST_WARNING ("A required element was not found: vp8enc. Skipping tests"); } tcase_add_test (tc, test_audio); tcase_add_test (tc, test_audio_sendrecv); tcase_add_test (tc, test_ice_port_restriction); tcase_add_test (tc, test_audio_video); tcase_add_test (tc, test_media_direction); tcase_add_test (tc, test_add_transceiver); tcase_add_test (tc, test_get_transceivers); tcase_add_test (tc, test_transceivers_mid); tcase_add_test (tc, test_add_recvonly_transceiver); tcase_add_test (tc, test_recvonly_sendonly); tcase_add_test (tc, test_payload_types); tcase_add_test (tc, test_bundle_audio_video_max_bundle_max_bundle); tcase_add_test (tc, test_bundle_audio_video_max_bundle_none); tcase_add_test (tc, test_bundle_audio_video_max_compat_max_bundle); tcase_add_test (tc, test_dual_audio); tcase_add_test (tc, test_duplicate_nego); tcase_add_test (tc, test_renego_add_stream); tcase_add_test (tc, test_bundle_renego_add_stream); tcase_add_test (tc, test_bundle_max_compat_max_bundle_renego_add_stream); tcase_add_test (tc, test_renego_transceiver_set_direction); tcase_add_test (tc, test_renego_triggering); tcase_add_test (tc, test_renego_lose_media_fails); tcase_add_test (tc, test_bundle_codec_preferences_rtx_no_duplicate_payloads); tcase_add_test (tc, test_reject_request_pad); tcase_add_test (tc, test_reject_create_offer); tcase_add_test (tc, test_reject_create_offer_mline_locked_no_caps); tcase_add_test (tc, test_reject_set_description); tcase_add_test (tc, test_force_second_media); tcase_add_test (tc, test_codec_preferences_caps); tcase_add_test (tc, test_codec_preferences_negotiation_sinkpad); tcase_add_test (tc, test_codec_preferences_negotiation_srcpad); tcase_add_test (tc, test_codec_preferences_in_on_new_transceiver); tcase_add_test (tc, test_codec_preferences_no_duplicate_extmaps); tcase_add_test (tc, test_codec_preferences_incompatible_extmaps); tcase_add_test (tc, test_codec_preferences_invalid_extmap); tcase_add_test (tc, test_renego_rtx); tcase_add_test (tc, test_bundle_mid_header_extension); tcase_add_test (tc, test_max_bundle_fec); tcase_add_test (tc, test_simulcast); tcase_add_test (tc, test_simulcast_fec_rtx); tcase_add_test (tc, test_bundle_multiple_media_rtx_payload_mapping); tcase_add_test (tc, test_invalid_add_media_in_answer); tcase_add_test (tc, test_add_turn_server); tcase_add_test (tc, test_msid); tcase_add_test (tc, test_ice_end_of_candidates); tcase_add_test (tc, test_sdp_session_setup_attribute); tcase_add_test (tc, test_rtp_header_extension_sendonly_recvonly_pair); tcase_add_test (tc, test_invalid_bundle_in_pending_remote_description); if (sctpenc && sctpdec) { tcase_add_test (tc, test_data_channel_create); tcase_add_test (tc, test_data_channel_create_two_channels); tcase_add_test (tc, test_data_channel_remote_notify); tcase_add_test (tc, test_data_channel_transfer_string); tcase_add_test (tc, test_data_channel_transfer_data); tcase_add_test (tc, test_data_channel_create_after_negotiate); tcase_add_test (tc, test_data_channel_close); tcase_add_test (tc, test_data_channel_low_threshold); tcase_add_test (tc, test_data_channel_max_message_size); tcase_add_test (tc, test_data_channel_pre_negotiated); tcase_add_test (tc, test_bundle_audio_video_data); tcase_add_test (tc, test_renego_stream_add_data_channel); tcase_add_test (tc, test_renego_data_channel_add_stream); tcase_add_test (tc, test_renego_stream_data_channel_add_stream); tcase_add_test (tc, test_data_channel_recreate_offer); } else { GST_WARNING ("Some required elements were not found. " "All datachannel tests are disabled. sctpenc %p, sctpdec %p", sctpenc, sctpdec); } tcase_add_test (tc, test_offer_rollback); } else { GST_WARNING ("Some required elements were not found. " "All media tests are disabled. nicesrc %p, nicesink %p, " "dtlssrtpenc %p, dtlssrtpdec %p", nicesrc, nicesink, dtlssrtpenc, dtlssrtpdec); } if (nicesrc) gst_object_unref (nicesrc); if (nicesink) gst_object_unref (nicesink); if (dtlssrtpdec) gst_object_unref (dtlssrtpdec); if (dtlssrtpenc) gst_object_unref (dtlssrtpenc); if (sctpenc) gst_object_unref (sctpenc); if (sctpdec) gst_object_unref (sctpdec); suite_add_tcase (s, tc); return s; } GST_CHECK_MAIN (webrtcbin);