summary refs log tree commit diff
path: root/src/WebRTCSession.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/WebRTCSession.cpp')
-rw-r--r--src/WebRTCSession.cpp1622
1 files changed, 796 insertions, 826 deletions
diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp

index 72330435..801a365c 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp
@@ -43,47 +43,47 @@ using webrtc::State; WebRTCSession::WebRTCSession() : devices_(CallDevices::instance()) { - qRegisterMetaType<webrtc::CallType>(); - qmlRegisterUncreatableMetaObject( - webrtc::staticMetaObject, "im.nheko", 1, 0, "CallType", "Can't instantiate enum"); + qRegisterMetaType<webrtc::CallType>(); + qmlRegisterUncreatableMetaObject( + webrtc::staticMetaObject, "im.nheko", 1, 0, "CallType", "Can't instantiate enum"); - qRegisterMetaType<webrtc::State>(); - qmlRegisterUncreatableMetaObject( - webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum"); + qRegisterMetaType<webrtc::State>(); + qmlRegisterUncreatableMetaObject( + webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum"); - connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); - init(); + connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); + init(); } bool WebRTCSession::init(std::string *errorMessage) { #ifdef GSTREAMER_AVAILABLE - if (initialised_) - return true; + if (initialised_) + return true; - GError *error = nullptr; - if (!gst_init_check(nullptr, nullptr, &error)) { - std::string strError("WebRTC: failed to initialise GStreamer: "); - if (error) { - strError += error->message; - g_error_free(error); - } - nhlog::ui()->error(strError); - if (errorMessage) - *errorMessage = strError; - return false; + GError *error = nullptr; + if (!gst_init_check(nullptr, nullptr, &error)) { + std::string strError("WebRTC: failed to initialise GStreamer: "); + if (error) { + strError += error->message; + g_error_free(error); } - - initialised_ = true; - gchar *version = gst_version_string(); - nhlog::ui()->info("WebRTC: initialised {}", version); - g_free(version); - devices_.init(); - return true; -#else - (void)errorMessage; + nhlog::ui()->error(strError); + if (errorMessage) + *errorMessage = strError; return false; + } + + initialised_ = true; + gchar *version = gst_version_string(); + nhlog::ui()->info("WebRTC: initialised {}", version); + g_free(version); + devices_.init(); + return true; +#else + (void)errorMessage; + return false; #endif } @@ -100,81 +100,77 @@ GstPad *remotePiPSinkPad_ = nullptr; gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data) { - WebRTCSession *session = static_cast<WebRTCSession *>(user_data); - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_EOS: - nhlog::ui()->error("WebRTC: end of stream"); - session->end(); - break; - case GST_MESSAGE_ERROR: - GError *error; - gchar *debug; - gst_message_parse_error(msg, &error, &debug); - nhlog::ui()->error( - "WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message); - g_clear_error(&error); - g_free(debug); - session->end(); - break; - default: - break; - } - return TRUE; + WebRTCSession *session = static_cast<WebRTCSession *>(user_data); + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_EOS: + nhlog::ui()->error("WebRTC: end of stream"); + session->end(); + break; + case GST_MESSAGE_ERROR: + GError *error; + gchar *debug; + gst_message_parse_error(msg, &error, &debug); + nhlog::ui()->error( + "WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message); + g_clear_error(&error); + g_free(debug); + session->end(); + break; + default: + break; + } + return TRUE; } GstWebRTCSessionDescription * parseSDP(const std::string &sdp, GstWebRTCSDPType type) { - GstSDPMessage *msg; - gst_sdp_message_new(&msg); - if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) { - return gst_webrtc_session_description_new(type, msg); - } else { - nhlog::ui()->error("WebRTC: failed to parse remote session description"); - gst_sdp_message_free(msg); - return nullptr; - } + GstSDPMessage *msg; + gst_sdp_message_new(&msg); + if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) { + return gst_webrtc_session_description_new(type, msg); + } else { + nhlog::ui()->error("WebRTC: failed to parse remote session description"); + gst_sdp_message_free(msg); + return nullptr; + } } void setLocalDescription(GstPromise *promise, gpointer webrtc) { - const GstStructure *reply = gst_promise_get_reply(promise); - gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer")); - GstWebRTCSessionDescription *gstsdp = nullptr; - gst_structure_get(reply, - isAnswer ? "answer" : "offer", - GST_TYPE_WEBRTC_SESSION_DESCRIPTION, - &gstsdp, - nullptr); - gst_promise_unref(promise); - g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr); - - gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp); - localsdp_ = std::string(sdp); - g_free(sdp); - gst_webrtc_session_description_free(gstsdp); - - nhlog::ui()->debug( - "WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_); + const GstStructure *reply = gst_promise_get_reply(promise); + gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer")); + GstWebRTCSessionDescription *gstsdp = nullptr; + gst_structure_get( + reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr); + gst_promise_unref(promise); + g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr); + + gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp); + localsdp_ = std::string(sdp); + g_free(sdp); + gst_webrtc_session_description_free(gstsdp); + + nhlog::ui()->debug( + "WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_); } void createOffer(GstElement *webrtc) { - // create-offer first, then set-local-description - GstPromise *promise = - gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); - g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise); + // create-offer first, then set-local-description + GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); + g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise); } void createAnswer(GstPromise *promise, gpointer webrtc) { - // create-answer first, then set-local-description - gst_promise_unref(promise); - promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); - g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise); + // create-answer first, then set-local-description + gst_promise_unref(promise); + promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); + g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise); } void @@ -182,18 +178,18 @@ iceGatheringStateChanged(GstElement *webrtc, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { - GstWebRTCICEGatheringState newState; - g_object_get(webrtc, "ice-gathering-state", &newState, nullptr); - if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) { - nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete"); - if (WebRTCSession::instance().isOffering()) { - emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged(State::OFFERSENT); - } else { - emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged(State::ANSWERSENT); - } + GstWebRTCICEGatheringState newState; + g_object_get(webrtc, "ice-gathering-state", &newState, nullptr); + if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) { + nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete"); + if (WebRTCSession::instance().isOffering()) { + emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); + emit WebRTCSession::instance().stateChanged(State::OFFERSENT); + } else { + emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); + emit WebRTCSession::instance().stateChanged(State::ANSWERSENT); } + } } void @@ -202,8 +198,8 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, gchar *candidate, gpointer G_GNUC_UNUSED) { - nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate); - localcandidates_.push_back({std::string() /*max-bundle*/, (uint16_t)mlineIndex, candidate}); + nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate); + localcandidates_.push_back({std::string() /*max-bundle*/, (uint16_t)mlineIndex, candidate}); } void @@ -211,337 +207,327 @@ iceConnectionStateChanged(GstElement *webrtc, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { - GstWebRTCICEConnectionState newState; - g_object_get(webrtc, "ice-connection-state", &newState, nullptr); - switch (newState) { - case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING: - nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking"); - emit WebRTCSession::instance().stateChanged(State::CONNECTING); - break; - case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED: - nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed"); - emit WebRTCSession::instance().stateChanged(State::ICEFAILED); - break; - default: - break; - } + GstWebRTCICEConnectionState newState; + g_object_get(webrtc, "ice-connection-state", &newState, nullptr); + switch (newState) { + case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING: + nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking"); + emit WebRTCSession::instance().stateChanged(State::CONNECTING); + break; + case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED: + nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed"); + emit WebRTCSession::instance().stateChanged(State::ICEFAILED); + break; + default: + break; + } } // https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1164 struct KeyFrameRequestData { - GstElement *pipe = nullptr; - GstElement *decodebin = nullptr; - gint packetsLost = 0; - guint timerid = 0; - std::string statsField; + GstElement *pipe = nullptr; + GstElement *decodebin = nullptr; + gint packetsLost = 0; + guint timerid = 0; + std::string statsField; } keyFrameRequestData_; void sendKeyFrameRequest() { - GstPad *sinkpad = gst_element_get_static_pad(keyFrameRequestData_.decodebin, "sink"); - if (!gst_pad_push_event(sinkpad, - gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, - gst_structure_new_empty("GstForceKeyUnit")))) - nhlog::ui()->error("WebRTC: key frame request failed"); - else - nhlog::ui()->debug("WebRTC: sent key frame request"); - - gst_object_unref(sinkpad); + GstPad *sinkpad = gst_element_get_static_pad(keyFrameRequestData_.decodebin, "sink"); + if (!gst_pad_push_event(sinkpad, + gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, + gst_structure_new_empty("GstForceKeyUnit")))) + nhlog::ui()->error("WebRTC: key frame request failed"); + else + nhlog::ui()->debug("WebRTC: sent key frame request"); + + gst_object_unref(sinkpad); } void testPacketLoss_(GstPromise *promise, gpointer G_GNUC_UNUSED) { - const GstStructure *reply = gst_promise_get_reply(promise); - gint packetsLost = 0; - GstStructure *rtpStats; - if (!gst_structure_get(reply, - keyFrameRequestData_.statsField.c_str(), - GST_TYPE_STRUCTURE, - &rtpStats, - nullptr)) { - nhlog::ui()->error("WebRTC: get-stats: no field: {}", - keyFrameRequestData_.statsField); - gst_promise_unref(promise); - return; - } - gst_structure_get_int(rtpStats, "packets-lost", &packetsLost); - gst_structure_free(rtpStats); + const GstStructure *reply = gst_promise_get_reply(promise); + gint packetsLost = 0; + GstStructure *rtpStats; + if (!gst_structure_get( + reply, keyFrameRequestData_.statsField.c_str(), GST_TYPE_STRUCTURE, &rtpStats, nullptr)) { + nhlog::ui()->error("WebRTC: get-stats: no field: {}", keyFrameRequestData_.statsField); gst_promise_unref(promise); - if (packetsLost > keyFrameRequestData_.packetsLost) { - nhlog::ui()->debug("WebRTC: inbound video lost packet count: {}", packetsLost); - keyFrameRequestData_.packetsLost = packetsLost; - sendKeyFrameRequest(); - } + return; + } + gst_structure_get_int(rtpStats, "packets-lost", &packetsLost); + gst_structure_free(rtpStats); + gst_promise_unref(promise); + if (packetsLost > keyFrameRequestData_.packetsLost) { + nhlog::ui()->debug("WebRTC: inbound video lost packet count: {}", packetsLost); + keyFrameRequestData_.packetsLost = packetsLost; + sendKeyFrameRequest(); + } } gboolean testPacketLoss(gpointer G_GNUC_UNUSED) { - if (keyFrameRequestData_.pipe) { - GstElement *webrtc = - gst_bin_get_by_name(GST_BIN(keyFrameRequestData_.pipe), "webrtcbin"); - GstPromise *promise = - gst_promise_new_with_change_func(testPacketLoss_, nullptr, nullptr); - g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise); - gst_object_unref(webrtc); - return TRUE; - } - return FALSE; + if (keyFrameRequestData_.pipe) { + GstElement *webrtc = gst_bin_get_by_name(GST_BIN(keyFrameRequestData_.pipe), "webrtcbin"); + GstPromise *promise = gst_promise_new_with_change_func(testPacketLoss_, nullptr, nullptr); + g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise); + gst_object_unref(webrtc); + return TRUE; + } + return FALSE; } void setWaitForKeyFrame(GstBin *decodebin G_GNUC_UNUSED, GstElement *element, gpointer G_GNUC_UNUSED) { - if (!std::strcmp( - gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))), - "rtpvp8depay")) - g_object_set(element, "wait-for-keyframe", TRUE, nullptr); + if (!std::strcmp( + gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))), + "rtpvp8depay")) + g_object_set(element, "wait-for-keyframe", TRUE, nullptr); } GstElement * newAudioSinkChain(GstElement *pipe) { - GstElement *queue = gst_element_factory_make("queue", nullptr); - GstElement *convert = gst_element_factory_make("audioconvert", nullptr); - GstElement *resample = gst_element_factory_make("audioresample", nullptr); - GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr); - gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr); - gst_element_link_many(queue, convert, resample, sink, nullptr); - gst_element_sync_state_with_parent(queue); - gst_element_sync_state_with_parent(convert); - gst_element_sync_state_with_parent(resample); - gst_element_sync_state_with_parent(sink); - return queue; + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *convert = gst_element_factory_make("audioconvert", nullptr); + GstElement *resample = gst_element_factory_make("audioresample", nullptr); + GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr); + gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr); + gst_element_link_many(queue, convert, resample, sink, nullptr); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(convert); + gst_element_sync_state_with_parent(resample); + gst_element_sync_state_with_parent(sink); + return queue; } GstElement * newVideoSinkChain(GstElement *pipe) { - // use compositor for now; acceleration needs investigation - GstElement *queue = gst_element_factory_make("queue", nullptr); - GstElement *compositor = gst_element_factory_make("compositor", "compositor"); - GstElement *glupload = gst_element_factory_make("glupload", nullptr); - GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr); - GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); - GstElement *glsinkbin = gst_element_factory_make("glsinkbin", nullptr); - g_object_set(compositor, "background", 1, nullptr); - g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr); - g_object_set(glsinkbin, "sink", qmlglsink, nullptr); - gst_bin_add_many( - GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); - gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); - gst_element_sync_state_with_parent(queue); - gst_element_sync_state_with_parent(compositor); - gst_element_sync_state_with_parent(glupload); - gst_element_sync_state_with_parent(glcolorconvert); - gst_element_sync_state_with_parent(glsinkbin); - return queue; + // use compositor for now; acceleration needs investigation + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *compositor = gst_element_factory_make("compositor", "compositor"); + GstElement *glupload = gst_element_factory_make("glupload", nullptr); + GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr); + GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); + GstElement *glsinkbin = gst_element_factory_make("glsinkbin", nullptr); + g_object_set(compositor, "background", 1, nullptr); + g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr); + g_object_set(glsinkbin, "sink", qmlglsink, nullptr); + gst_bin_add_many( + GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); + gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(compositor); + gst_element_sync_state_with_parent(glupload); + gst_element_sync_state_with_parent(glcolorconvert); + gst_element_sync_state_with_parent(glsinkbin); + return queue; } std::pair<int, int> getResolution(GstPad *pad) { - std::pair<int, int> ret; - GstCaps *caps = gst_pad_get_current_caps(pad); - const GstStructure *s = gst_caps_get_structure(caps, 0); - gst_structure_get_int(s, "width", &ret.first); - gst_structure_get_int(s, "height", &ret.second); - gst_caps_unref(caps); - return ret; + std::pair<int, int> ret; + GstCaps *caps = gst_pad_get_current_caps(pad); + const GstStructure *s = gst_caps_get_structure(caps, 0); + gst_structure_get_int(s, "width", &ret.first); + gst_structure_get_int(s, "height", &ret.second); + gst_caps_unref(caps); + return ret; } std::pair<int, int> getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName) { - GstElement *element = gst_bin_get_by_name(GST_BIN(pipe), elementName); - GstPad *pad = gst_element_get_static_pad(element, padName); - auto ret = getResolution(pad); - gst_object_unref(pad); - gst_object_unref(element); - return ret; + GstElement *element = gst_bin_get_by_name(GST_BIN(pipe), elementName); + GstPad *pad = gst_element_get_static_pad(element, padName); + auto ret = getResolution(pad); + gst_object_unref(pad); + gst_object_unref(element); + return ret; } std::pair<int, int> getPiPDimensions(const std::pair<int, int> &resolution, int fullWidth, double scaleFactor) { - int pipWidth = fullWidth * scaleFactor; - int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth; - return {pipWidth, pipHeight}; + int pipWidth = fullWidth * scaleFactor; + int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth; + return {pipWidth, pipHeight}; } void addLocalPiP(GstElement *pipe, const std::pair<int, int> &videoCallSize) { - // embed localUser's camera into received video (CallType::VIDEO) - // OR embed screen share into received video (CallType::SCREEN) - GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); - if (!tee) - return; - - GstElement *queue = gst_element_factory_make("queue", nullptr); - gst_bin_add(GST_BIN(pipe), queue); - gst_element_link(tee, queue); - gst_element_sync_state_with_parent(queue); - gst_object_unref(tee); - - GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor"); - localPiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); - g_object_set(localPiPSinkPad_, "zorder", 2, nullptr); - - bool isVideo = WebRTCSession::instance().callType() == CallType::VIDEO; - const gchar *element = isVideo ? "camerafilter" : "screenshare"; - const gchar *pad = isVideo ? "sink" : "src"; - auto resolution = getResolution(pipe, element, pad); - auto pipSize = getPiPDimensions(resolution, videoCallSize.first, 0.25); - nhlog::ui()->debug( - "WebRTC: local picture-in-picture: {}x{}", pipSize.first, pipSize.second); - g_object_set(localPiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); - gint offset = videoCallSize.first / 80; - g_object_set(localPiPSinkPad_, "xpos", offset, "ypos", offset, nullptr); - - GstPad *srcpad = gst_element_get_static_pad(queue, "src"); - if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, localPiPSinkPad_))) - nhlog::ui()->error("WebRTC: failed to link local PiP elements"); - gst_object_unref(srcpad); - gst_object_unref(compositor); + // embed localUser's camera into received video (CallType::VIDEO) + // OR embed screen share into received video (CallType::SCREEN) + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); + if (!tee) + return; + + GstElement *queue = gst_element_factory_make("queue", nullptr); + gst_bin_add(GST_BIN(pipe), queue); + gst_element_link(tee, queue); + gst_element_sync_state_with_parent(queue); + gst_object_unref(tee); + + GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor"); + localPiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); + g_object_set(localPiPSinkPad_, "zorder", 2, nullptr); + + bool isVideo = WebRTCSession::instance().callType() == CallType::VIDEO; + const gchar *element = isVideo ? "camerafilter" : "screenshare"; + const gchar *pad = isVideo ? "sink" : "src"; + auto resolution = getResolution(pipe, element, pad); + auto pipSize = getPiPDimensions(resolution, videoCallSize.first, 0.25); + nhlog::ui()->debug("WebRTC: local picture-in-picture: {}x{}", pipSize.first, pipSize.second); + g_object_set(localPiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); + gint offset = videoCallSize.first / 80; + g_object_set(localPiPSinkPad_, "xpos", offset, "ypos", offset, nullptr); + + GstPad *srcpad = gst_element_get_static_pad(queue, "src"); + if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, localPiPSinkPad_))) + nhlog::ui()->error("WebRTC: failed to link local PiP elements"); + gst_object_unref(srcpad); + gst_object_unref(compositor); } void addRemotePiP(GstElement *pipe) { - // embed localUser's camera into screen image being shared - if (remotePiPSinkPad_) { - auto camRes = getResolution(pipe, "camerafilter", "sink"); - auto shareRes = getResolution(pipe, "screenshare", "src"); - auto pipSize = getPiPDimensions(camRes, shareRes.first, 0.2); - nhlog::ui()->debug( - "WebRTC: screen share picture-in-picture: {}x{}", pipSize.first, pipSize.second); - - gint offset = shareRes.first / 100; - g_object_set(remotePiPSinkPad_, "zorder", 2, nullptr); - g_object_set( - remotePiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); - g_object_set(remotePiPSinkPad_, - "xpos", - shareRes.first - pipSize.first - offset, - "ypos", - shareRes.second - pipSize.second - offset, - nullptr); - } + // embed localUser's camera into screen image being shared + if (remotePiPSinkPad_) { + auto camRes = getResolution(pipe, "camerafilter", "sink"); + auto shareRes = getResolution(pipe, "screenshare", "src"); + auto pipSize = getPiPDimensions(camRes, shareRes.first, 0.2); + nhlog::ui()->debug( + "WebRTC: screen share picture-in-picture: {}x{}", pipSize.first, pipSize.second); + + gint offset = shareRes.first / 100; + g_object_set(remotePiPSinkPad_, "zorder", 2, nullptr); + g_object_set(remotePiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); + g_object_set(remotePiPSinkPad_, + "xpos", + shareRes.first - pipSize.first - offset, + "ypos", + shareRes.second - pipSize.second - offset, + nullptr); + } } void addLocalVideo(GstElement *pipe) { - GstElement *queue = newVideoSinkChain(pipe); - GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); - GstPad *srcpad = gst_element_get_request_pad(tee, "src_%u"); - GstPad *sinkpad = gst_element_get_static_pad(queue, "sink"); - if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, sinkpad))) - nhlog::ui()->error("WebRTC: failed to link videosrctee -> video sink chain"); - gst_object_unref(srcpad); + GstElement *queue = newVideoSinkChain(pipe); + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); + GstPad *srcpad = gst_element_get_request_pad(tee, "src_%u"); + GstPad *sinkpad = gst_element_get_static_pad(queue, "sink"); + if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, sinkpad))) + nhlog::ui()->error("WebRTC: failed to link videosrctee -> video sink chain"); + gst_object_unref(srcpad); } void linkNewPad(GstElement *decodebin, GstPad *newpad, GstElement *pipe) { - GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); - GstCaps *sinkcaps = gst_pad_get_current_caps(sinkpad); - const GstStructure *structure = gst_caps_get_structure(sinkcaps, 0); - - gchar *mediaType = nullptr; - guint ssrc = 0; - gst_structure_get( - structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr); - gst_caps_unref(sinkcaps); - gst_object_unref(sinkpad); - - WebRTCSession *session = &WebRTCSession::instance(); - GstElement *queue = nullptr; - if (!std::strcmp(mediaType, "audio")) { - nhlog::ui()->debug("WebRTC: received incoming audio stream"); - haveAudioStream_ = true; - queue = newAudioSinkChain(pipe); - } else if (!std::strcmp(mediaType, "video")) { - nhlog::ui()->debug("WebRTC: received incoming video stream"); - if (!session->getVideoItem()) { - g_free(mediaType); - nhlog::ui()->error("WebRTC: video call item not set"); - return; - } - haveVideoStream_ = true; - keyFrameRequestData_.statsField = - std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc); - queue = newVideoSinkChain(pipe); - auto videoCallSize = getResolution(newpad); - nhlog::ui()->info("WebRTC: incoming video resolution: {}x{}", - videoCallSize.first, - videoCallSize.second); - addLocalPiP(pipe, videoCallSize); - } else { - g_free(mediaType); - nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad)); - return; + GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); + GstCaps *sinkcaps = gst_pad_get_current_caps(sinkpad); + const GstStructure *structure = gst_caps_get_structure(sinkcaps, 0); + + gchar *mediaType = nullptr; + guint ssrc = 0; + gst_structure_get( + structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr); + gst_caps_unref(sinkcaps); + gst_object_unref(sinkpad); + + WebRTCSession *session = &WebRTCSession::instance(); + GstElement *queue = nullptr; + if (!std::strcmp(mediaType, "audio")) { + nhlog::ui()->debug("WebRTC: received incoming audio stream"); + haveAudioStream_ = true; + queue = newAudioSinkChain(pipe); + } else if (!std::strcmp(mediaType, "video")) { + nhlog::ui()->debug("WebRTC: received incoming video stream"); + if (!session->getVideoItem()) { + g_free(mediaType); + nhlog::ui()->error("WebRTC: video call item not set"); + return; } - - GstPad *queuepad = gst_element_get_static_pad(queue, "sink"); - if (queuepad) { - if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad))) - nhlog::ui()->error("WebRTC: unable to link new pad"); - else { - if (session->callType() == CallType::VOICE || - (haveAudioStream_ && - (haveVideoStream_ || session->isRemoteVideoRecvOnly()))) { - emit session->stateChanged(State::CONNECTED); - if (haveVideoStream_) { - keyFrameRequestData_.pipe = pipe; - keyFrameRequestData_.decodebin = decodebin; - keyFrameRequestData_.timerid = - g_timeout_add_seconds(3, testPacketLoss, nullptr); - } - addRemotePiP(pipe); - if (session->isRemoteVideoRecvOnly()) - addLocalVideo(pipe); - } + haveVideoStream_ = true; + keyFrameRequestData_.statsField = + std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc); + queue = newVideoSinkChain(pipe); + auto videoCallSize = getResolution(newpad); + nhlog::ui()->info( + "WebRTC: incoming video resolution: {}x{}", videoCallSize.first, videoCallSize.second); + addLocalPiP(pipe, videoCallSize); + } else { + g_free(mediaType); + nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad)); + return; + } + + GstPad *queuepad = gst_element_get_static_pad(queue, "sink"); + if (queuepad) { + if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad))) + nhlog::ui()->error("WebRTC: unable to link new pad"); + else { + if (session->callType() == CallType::VOICE || + (haveAudioStream_ && (haveVideoStream_ || session->isRemoteVideoRecvOnly()))) { + emit session->stateChanged(State::CONNECTED); + if (haveVideoStream_) { + keyFrameRequestData_.pipe = pipe; + keyFrameRequestData_.decodebin = decodebin; + keyFrameRequestData_.timerid = + g_timeout_add_seconds(3, testPacketLoss, nullptr); } - gst_object_unref(queuepad); + addRemotePiP(pipe); + if (session->isRemoteVideoRecvOnly()) + addLocalVideo(pipe); + } } - g_free(mediaType); + gst_object_unref(queuepad); + } + g_free(mediaType); } void addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe) { - if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) - return; - - nhlog::ui()->debug("WebRTC: received incoming stream"); - GstElement *decodebin = gst_element_factory_make("decodebin", nullptr); - // hardware decoding needs investigation; eg rendering fails if vaapi plugin installed - g_object_set(decodebin, "force-sw-decoders", TRUE, nullptr); - g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe); - g_signal_connect(decodebin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr); - gst_bin_add(GST_BIN(pipe), decodebin); - gst_element_sync_state_with_parent(decodebin); - GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); - if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) - nhlog::ui()->error("WebRTC: unable to link decodebin"); - gst_object_unref(sinkpad); + if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) + return; + + nhlog::ui()->debug("WebRTC: received incoming stream"); + GstElement *decodebin = gst_element_factory_make("decodebin", nullptr); + // hardware decoding needs investigation; eg rendering fails if vaapi plugin installed + g_object_set(decodebin, "force-sw-decoders", TRUE, nullptr); + g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe); + g_signal_connect(decodebin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr); + gst_bin_add(GST_BIN(pipe), decodebin); + gst_element_sync_state_with_parent(decodebin); + GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); + if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) + nhlog::ui()->error("WebRTC: unable to link decodebin"); + gst_object_unref(sinkpad); } bool contains(std::string_view str1, std::string_view str2) { - return std::search(str1.cbegin(), - str1.cend(), - str2.cbegin(), - str2.cend(), - [](unsigned char c1, unsigned char c2) { - return std::tolower(c1) == std::tolower(c2); - }) != str1.cend(); + return std::search(str1.cbegin(), + str1.cend(), + str2.cbegin(), + str2.cend(), + [](unsigned char c1, unsigned char c2) { + return std::tolower(c1) == std::tolower(c2); + }) != str1.cend(); } bool @@ -552,582 +538,566 @@ getMediaAttributes(const GstSDPMessage *sdp, bool &recvOnly, bool &sendOnly) { - payloadType = -1; - recvOnly = false; - sendOnly = false; - for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); ++mlineIndex) { - const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex); - if (!std::strcmp(gst_sdp_media_get_media(media), mediaType)) { - recvOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr; - sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr; - const gchar *rtpval = nullptr; - for (guint n = 0; n == 0 || rtpval; ++n) { - rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n); - if (rtpval && contains(rtpval, encoding)) { - payloadType = std::atoi(rtpval); - break; - } - } - return true; + payloadType = -1; + recvOnly = false; + sendOnly = false; + for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); ++mlineIndex) { + const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex); + if (!std::strcmp(gst_sdp_media_get_media(media), mediaType)) { + recvOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr; + sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr; + const gchar *rtpval = nullptr; + for (guint n = 0; n == 0 || rtpval; ++n) { + rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n); + if (rtpval && contains(rtpval, encoding)) { + payloadType = std::atoi(rtpval); + break; } + } + return true; } - return false; + } + return false; } } bool WebRTCSession::havePlugins(bool isVideo, std::string *errorMessage) { - if (!initialised_ && !init(errorMessage)) - return false; - if (!isVideo && haveVoicePlugins_) - return true; - if (isVideo && haveVideoPlugins_) - return true; - - const gchar *voicePlugins[] = {"audioconvert", - "audioresample", - "autodetect", - "dtls", - "nice", - "opus", - "playback", - "rtpmanager", - "srtp", - "volume", - "webrtc", - nullptr}; - - const gchar *videoPlugins[] = { - "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr}; - - std::string strError("Missing GStreamer plugins: "); - const gchar **needed = isVideo ? videoPlugins : voicePlugins; - bool &havePlugins = isVideo ? haveVideoPlugins_ : haveVoicePlugins_; - havePlugins = true; - GstRegistry *registry = gst_registry_get(); - for (guint i = 0; i < g_strv_length((gchar **)needed); i++) { - GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); - if (!plugin) { - havePlugins = false; - strError += std::string(needed[i]) + " "; - continue; - } - gst_object_unref(plugin); - } - if (!havePlugins) { - nhlog::ui()->error(strError); - if (errorMessage) - *errorMessage = strError; - return false; - } + if (!initialised_ && !init(errorMessage)) + return false; + if (!isVideo && haveVoicePlugins_) + return true; + if (isVideo && haveVideoPlugins_) + return true; - if (isVideo) { - // load qmlglsink to register GStreamer's GstGLVideoItem QML type - GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); - gst_object_unref(qmlglsink); + const gchar *voicePlugins[] = {"audioconvert", + "audioresample", + "autodetect", + "dtls", + "nice", + "opus", + "playback", + "rtpmanager", + "srtp", + "volume", + "webrtc", + nullptr}; + + const gchar *videoPlugins[] = { + "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr}; + + std::string strError("Missing GStreamer plugins: "); + const gchar **needed = isVideo ? videoPlugins : voicePlugins; + bool &havePlugins = isVideo ? haveVideoPlugins_ : haveVoicePlugins_; + havePlugins = true; + GstRegistry *registry = gst_registry_get(); + for (guint i = 0; i < g_strv_length((gchar **)needed); i++) { + GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); + if (!plugin) { + havePlugins = false; + strError += std::string(needed[i]) + " "; + continue; } - return true; + gst_object_unref(plugin); + } + if (!havePlugins) { + nhlog::ui()->error(strError); + if (errorMessage) + *errorMessage = strError; + return false; + } + + if (isVideo) { + // load qmlglsink to register GStreamer's GstGLVideoItem QML type + GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); + gst_object_unref(qmlglsink); + } + return true; } bool WebRTCSession::createOffer(CallType callType, uint32_t shareWindowId) { - clear(); - isOffering_ = true; - callType_ = callType; - shareWindowId_ = shareWindowId; - - // opus and vp8 rtp payload types must be defined dynamically - // therefore from the range [96-127] - // see for example https://tools.ietf.org/html/rfc7587 - constexpr int opusPayloadType = 111; - constexpr int vp8PayloadType = 96; - return startPipeline(opusPayloadType, vp8PayloadType); + clear(); + isOffering_ = true; + callType_ = callType; + shareWindowId_ = shareWindowId; + + // opus and vp8 rtp payload types must be defined dynamically + // therefore from the range [96-127] + // see for example https://tools.ietf.org/html/rfc7587 + constexpr int opusPayloadType = 111; + constexpr int vp8PayloadType = 96; + return startPipeline(opusPayloadType, vp8PayloadType); } bool WebRTCSession::acceptOffer(const std::string &sdp) { - nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp); - if (state_ != State::DISCONNECTED) - return false; + nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp); + if (state_ != State::DISCONNECTED) + return false; - clear(); - GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); - if (!offer) - return false; + clear(); + GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); + if (!offer) + return false; - int opusPayloadType; - bool recvOnly; - bool sendOnly; - if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) { - if (opusPayloadType == -1) { - nhlog::ui()->error("WebRTC: remote audio offer - no opus encoding"); - gst_webrtc_session_description_free(offer); - return false; - } - } else { - nhlog::ui()->error("WebRTC: remote offer - no audio media"); - gst_webrtc_session_description_free(offer); - return false; + int opusPayloadType; + bool recvOnly; + bool sendOnly; + if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) { + if (opusPayloadType == -1) { + nhlog::ui()->error("WebRTC: remote audio offer - no opus encoding"); + gst_webrtc_session_description_free(offer); + return false; } + } else { + nhlog::ui()->error("WebRTC: remote offer - no audio media"); + gst_webrtc_session_description_free(offer); + return false; + } - int vp8PayloadType; - bool isVideo = getMediaAttributes(offer->sdp, - "video", - "vp8", - vp8PayloadType, - isRemoteVideoRecvOnly_, - isRemoteVideoSendOnly_); - if (isVideo && vp8PayloadType == -1) { - nhlog::ui()->error("WebRTC: remote video offer - no vp8 encoding"); - gst_webrtc_session_description_free(offer); - return false; - } - callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; + int vp8PayloadType; + bool isVideo = getMediaAttributes( + offer->sdp, "video", "vp8", vp8PayloadType, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_); + if (isVideo && vp8PayloadType == -1) { + nhlog::ui()->error("WebRTC: remote video offer - no vp8 encoding"); + gst_webrtc_session_description_free(offer); + return false; + } + callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; - if (!startPipeline(opusPayloadType, vp8PayloadType)) { - gst_webrtc_session_description_free(offer); - return false; - } + if (!startPipeline(opusPayloadType, vp8PayloadType)) { + gst_webrtc_session_description_free(offer); + return false; + } - // avoid a race that sometimes leaves the generated answer without media tracks (a=ssrc - // lines) - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + // avoid a race that sometimes leaves the generated answer without media tracks (a=ssrc + // lines) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); - // set-remote-description first, then create-answer - GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr); - g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise); - gst_webrtc_session_description_free(offer); - return true; + // set-remote-description first, then create-answer + GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr); + g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise); + gst_webrtc_session_description_free(offer); + return true; } bool WebRTCSession::acceptAnswer(const std::string &sdp) { - nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp); - if (state_ != State::OFFERSENT) - return false; - - GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER); - if (!answer) { - end(); - return false; - } - - if (callType_ != CallType::VOICE) { - int unused; - if (!getMediaAttributes(answer->sdp, - "video", - "vp8", - unused, - isRemoteVideoRecvOnly_, - isRemoteVideoSendOnly_)) - isRemoteVideoRecvOnly_ = true; - } + nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp); + if (state_ != State::OFFERSENT) + return false; - g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr); - gst_webrtc_session_description_free(answer); - return true; + GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER); + if (!answer) { + end(); + return false; + } + + if (callType_ != CallType::VOICE) { + int unused; + if (!getMediaAttributes( + answer->sdp, "video", "vp8", unused, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_)) + isRemoteVideoRecvOnly_ = true; + } + + g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr); + gst_webrtc_session_description_free(answer); + return true; } void WebRTCSession::acceptICECandidates( const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates) { - if (state_ >= State::INITIATED) { - for (const auto &c : candidates) { - nhlog::ui()->debug( - "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate); - if (!c.candidate.empty()) { - g_signal_emit_by_name(webrtc_, - "add-ice-candidate", - c.sdpMLineIndex, - c.candidate.c_str()); - } - } + if (state_ >= State::INITIATED) { + for (const auto &c : candidates) { + nhlog::ui()->debug( + "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate); + if (!c.candidate.empty()) { + g_signal_emit_by_name( + webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str()); + } } + } } bool WebRTCSession::startPipeline(int opusPayloadType, int vp8PayloadType) { - if (state_ != State::DISCONNECTED) - return false; - - emit stateChanged(State::INITIATING); - - if (!createPipeline(opusPayloadType, vp8PayloadType)) { - end(); - return false; - } - - webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); - - if (ChatPage::instance()->userSettings()->useStunServer()) { - nhlog::ui()->info("WebRTC: setting STUN server: {}", STUN_SERVER); - g_object_set(webrtc_, "stun-server", STUN_SERVER, nullptr); - } - - for (const auto &uri : turnServers_) { - nhlog::ui()->info("WebRTC: setting TURN server: {}", uri); - gboolean udata; - g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata)); - } - if (turnServers_.empty()) - nhlog::ui()->warn("WebRTC: no TURN server provided"); - - // generate the offer when the pipeline goes to PLAYING - if (isOffering_) - g_signal_connect( - webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr); - - // on-ice-candidate is emitted when a local ICE candidate has been gathered - g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr); - - // capture ICE failure - g_signal_connect( - webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr); - - // incoming streams trigger pad-added - gst_element_set_state(pipe_, GST_STATE_READY); - g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_); - - // capture ICE gathering completion - g_signal_connect( - webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr); + if (state_ != State::DISCONNECTED) + return false; - // webrtcbin lifetime is the same as that of the pipeline - gst_object_unref(webrtc_); + emit stateChanged(State::INITIATING); - // start the pipeline - GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING); - if (ret == GST_STATE_CHANGE_FAILURE) { - nhlog::ui()->error("WebRTC: unable to start pipeline"); - end(); - return false; - } + if (!createPipeline(opusPayloadType, vp8PayloadType)) { + end(); + return false; + } + + webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); + + if (ChatPage::instance()->userSettings()->useStunServer()) { + nhlog::ui()->info("WebRTC: setting STUN server: {}", STUN_SERVER); + g_object_set(webrtc_, "stun-server", STUN_SERVER, nullptr); + } + + for (const auto &uri : turnServers_) { + nhlog::ui()->info("WebRTC: setting TURN server: {}", uri); + gboolean udata; + g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata)); + } + if (turnServers_.empty()) + nhlog::ui()->warn("WebRTC: no TURN server provided"); + + // generate the offer when the pipeline goes to PLAYING + if (isOffering_) + g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr); + + // on-ice-candidate is emitted when a local ICE candidate has been gathered + g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr); + + // capture ICE failure + g_signal_connect( + webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr); + + // incoming streams trigger pad-added + gst_element_set_state(pipe_, GST_STATE_READY); + g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_); + + // capture ICE gathering completion + g_signal_connect( + webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr); + + // webrtcbin lifetime is the same as that of the pipeline + gst_object_unref(webrtc_); + + // start the pipeline + GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING); + if (ret == GST_STATE_CHANGE_FAILURE) { + nhlog::ui()->error("WebRTC: unable to start pipeline"); + end(); + return false; + } - GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); - busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this); - gst_object_unref(bus); - emit stateChanged(State::INITIATED); - return true; + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); + busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this); + gst_object_unref(bus); + emit stateChanged(State::INITIATED); + return true; } bool WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType) { - GstDevice *device = devices_.audioDevice(); - if (!device) - return false; - - GstElement *source = gst_device_create_element(device, nullptr); - GstElement *volume = gst_element_factory_make("volume", "srclevel"); - GstElement *convert = gst_element_factory_make("audioconvert", nullptr); - GstElement *resample = gst_element_factory_make("audioresample", nullptr); - GstElement *queue1 = gst_element_factory_make("queue", nullptr); - GstElement *opusenc = gst_element_factory_make("opusenc", nullptr); - GstElement *rtp = gst_element_factory_make("rtpopuspay", nullptr); - GstElement *queue2 = gst_element_factory_make("queue", nullptr); - GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + GstDevice *device = devices_.audioDevice(); + if (!device) + return false; - GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", - "media", - G_TYPE_STRING, - "audio", - "encoding-name", - G_TYPE_STRING, - "OPUS", - "payload", - G_TYPE_INT, - opusPayloadType, - nullptr); - g_object_set(capsfilter, "caps", rtpcaps, nullptr); - gst_caps_unref(rtpcaps); - - GstElement *webrtcbin = gst_element_factory_make("webrtcbin", "webrtcbin"); - g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr); - - pipe_ = gst_pipeline_new(nullptr); - gst_bin_add_many(GST_BIN(pipe_), - source, - volume, - convert, - resample, - queue1, - opusenc, - rtp, - queue2, - capsfilter, - webrtcbin, - nullptr); - - if (!gst_element_link_many(source, - volume, - convert, - resample, - queue1, - opusenc, - rtp, - queue2, - capsfilter, - webrtcbin, - nullptr)) { - nhlog::ui()->error("WebRTC: failed to link audio pipeline elements"); - return false; - } + GstElement *source = gst_device_create_element(device, nullptr); + GstElement *volume = gst_element_factory_make("volume", "srclevel"); + GstElement *convert = gst_element_factory_make("audioconvert", nullptr); + GstElement *resample = gst_element_factory_make("audioresample", nullptr); + GstElement *queue1 = gst_element_factory_make("queue", nullptr); + GstElement *opusenc = gst_element_factory_make("opusenc", nullptr); + GstElement *rtp = gst_element_factory_make("rtpopuspay", nullptr); + GstElement *queue2 = gst_element_factory_make("queue", nullptr); + GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + + GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", + "media", + G_TYPE_STRING, + "audio", + "encoding-name", + G_TYPE_STRING, + "OPUS", + "payload", + G_TYPE_INT, + opusPayloadType, + nullptr); + g_object_set(capsfilter, "caps", rtpcaps, nullptr); + gst_caps_unref(rtpcaps); + + GstElement *webrtcbin = gst_element_factory_make("webrtcbin", "webrtcbin"); + g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr); + + pipe_ = gst_pipeline_new(nullptr); + gst_bin_add_many(GST_BIN(pipe_), + source, + volume, + convert, + resample, + queue1, + opusenc, + rtp, + queue2, + capsfilter, + webrtcbin, + nullptr); + + if (!gst_element_link_many(source, + volume, + convert, + resample, + queue1, + opusenc, + rtp, + queue2, + capsfilter, + webrtcbin, + nullptr)) { + nhlog::ui()->error("WebRTC: failed to link audio pipeline elements"); + return false; + } - return callType_ == CallType::VOICE || isRemoteVideoSendOnly_ - ? true - : addVideoPipeline(vp8PayloadType); + return callType_ == CallType::VOICE || isRemoteVideoSendOnly_ + ? true + : addVideoPipeline(vp8PayloadType); } bool WebRTCSession::addVideoPipeline(int vp8PayloadType) { - // allow incoming video calls despite localUser having no webcam - if (callType_ == CallType::VIDEO && !devices_.haveCamera()) - return !isOffering_; - - auto settings = ChatPage::instance()->userSettings(); - GstElement *camerafilter = nullptr; - GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); - GstElement *tee = gst_element_factory_make("tee", "videosrctee"); - gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr); - if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) { - std::pair<int, int> resolution; - std::pair<int, int> frameRate; - GstDevice *device = devices_.videoDevice(resolution, frameRate); - if (!device) - return false; - - GstElement *camera = gst_device_create_element(device, nullptr); - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "width", - G_TYPE_INT, - resolution.first, - "height", - G_TYPE_INT, - resolution.second, - "framerate", - GST_TYPE_FRACTION, - frameRate.first, - frameRate.second, - nullptr); - camerafilter = gst_element_factory_make("capsfilter", "camerafilter"); - g_object_set(camerafilter, "caps", caps, nullptr); - gst_caps_unref(caps); - - gst_bin_add_many(GST_BIN(pipe_), camera, camerafilter, nullptr); - if (!gst_element_link_many(camera, videoconvert, camerafilter, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link camera elements"); - return false; - } - if (callType_ == CallType::VIDEO && !gst_element_link(camerafilter, tee)) { - nhlog::ui()->error("WebRTC: failed to link camerafilter -> tee"); - return false; - } - } + // allow incoming video calls despite localUser having no webcam + if (callType_ == CallType::VIDEO && !devices_.haveCamera()) + return !isOffering_; + + auto settings = ChatPage::instance()->userSettings(); + GstElement *camerafilter = nullptr; + GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); + GstElement *tee = gst_element_factory_make("tee", "videosrctee"); + gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr); + if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) { + std::pair<int, int> resolution; + std::pair<int, int> frameRate; + GstDevice *device = devices_.videoDevice(resolution, frameRate); + if (!device) + return false; + + GstElement *camera = gst_device_create_element(device, nullptr); + GstCaps *caps = gst_caps_new_simple("video/x-raw", + "width", + G_TYPE_INT, + resolution.first, + "height", + G_TYPE_INT, + resolution.second, + "framerate", + GST_TYPE_FRACTION, + frameRate.first, + frameRate.second, + nullptr); + camerafilter = gst_element_factory_make("capsfilter", "camerafilter"); + g_object_set(camerafilter, "caps", caps, nullptr); + gst_caps_unref(caps); - if (callType_ == CallType::SCREEN) { - nhlog::ui()->debug("WebRTC: screen share frame rate: {} fps", - settings->screenShareFrameRate()); - nhlog::ui()->debug("WebRTC: screen share picture-in-picture: {}", - settings->screenSharePiP()); - nhlog::ui()->debug("WebRTC: screen share request remote camera: {}", - settings->screenShareRemoteVideo()); - nhlog::ui()->debug("WebRTC: screen share hide mouse cursor: {}", - settings->screenShareHideCursor()); - - GstElement *ximagesrc = gst_element_factory_make("ximagesrc", "screenshare"); - if (!ximagesrc) { - nhlog::ui()->error("WebRTC: failed to create ximagesrc"); - return false; - } - g_object_set(ximagesrc, "use-damage", FALSE, nullptr); - g_object_set(ximagesrc, "xid", shareWindowId_, nullptr); - g_object_set( - ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr); - - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "framerate", - GST_TYPE_FRACTION, - settings->screenShareFrameRate(), - 1, - nullptr); - GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); - g_object_set(capsfilter, "caps", caps, nullptr); - gst_caps_unref(caps); - gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr); - - if (settings->screenSharePiP() && devices_.haveCamera()) { - GstElement *compositor = gst_element_factory_make("compositor", nullptr); - g_object_set(compositor, "background", 1, nullptr); - gst_bin_add(GST_BIN(pipe_), compositor); - if (!gst_element_link_many( - ximagesrc, compositor, capsfilter, tee, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link screen share elements"); - return false; - } - - GstPad *srcpad = gst_element_get_static_pad(camerafilter, "src"); - remotePiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); - if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, remotePiPSinkPad_))) { - nhlog::ui()->error( - "WebRTC: failed to link camerafilter -> compositor"); - gst_object_unref(srcpad); - return false; - } - gst_object_unref(srcpad); - } else if (!gst_element_link_many( - ximagesrc, videoconvert, capsfilter, tee, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link screen share elements"); - return false; - } + gst_bin_add_many(GST_BIN(pipe_), camera, camerafilter, nullptr); + if (!gst_element_link_many(camera, videoconvert, camerafilter, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link camera elements"); + return false; } - - GstElement *queue = gst_element_factory_make("queue", nullptr); - GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr); - g_object_set(vp8enc, "deadline", 1, nullptr); - g_object_set(vp8enc, "error-resilient", 1, nullptr); - GstElement *rtpvp8pay = gst_element_factory_make("rtpvp8pay", nullptr); - GstElement *rtpqueue = gst_element_factory_make("queue", nullptr); - GstElement *rtpcapsfilter = gst_element_factory_make("capsfilter", nullptr); - GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", - "media", - G_TYPE_STRING, - "video", - "encoding-name", - G_TYPE_STRING, - "VP8", - "payload", - G_TYPE_INT, - vp8PayloadType, - nullptr); - g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr); - gst_caps_unref(rtpcaps); - - gst_bin_add_many( - GST_BIN(pipe_), queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, nullptr); - - GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); - if (!gst_element_link_many( - tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link rtp video elements"); - gst_object_unref(webrtcbin); - return false; + if (callType_ == CallType::VIDEO && !gst_element_link(camerafilter, tee)) { + nhlog::ui()->error("WebRTC: failed to link camerafilter -> tee"); + return false; } - - if (callType_ == CallType::SCREEN && - !ChatPage::instance()->userSettings()->screenShareRemoteVideo()) { - GArray *transceivers; - g_signal_emit_by_name(webrtcbin, "get-transceivers", &transceivers); - GstWebRTCRTPTransceiver *transceiver = - g_array_index(transceivers, GstWebRTCRTPTransceiver *, 1); - transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; - g_array_unref(transceivers); + } + + if (callType_ == CallType::SCREEN) { + nhlog::ui()->debug("WebRTC: screen share frame rate: {} fps", + settings->screenShareFrameRate()); + nhlog::ui()->debug("WebRTC: screen share picture-in-picture: {}", + settings->screenSharePiP()); + nhlog::ui()->debug("WebRTC: screen share request remote camera: {}", + settings->screenShareRemoteVideo()); + nhlog::ui()->debug("WebRTC: screen share hide mouse cursor: {}", + settings->screenShareHideCursor()); + + GstElement *ximagesrc = gst_element_factory_make("ximagesrc", "screenshare"); + if (!ximagesrc) { + nhlog::ui()->error("WebRTC: failed to create ximagesrc"); + return false; } + g_object_set(ximagesrc, "use-damage", FALSE, nullptr); + g_object_set(ximagesrc, "xid", shareWindowId_, nullptr); + g_object_set(ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr); + + GstCaps *caps = gst_caps_new_simple("video/x-raw", + "framerate", + GST_TYPE_FRACTION, + settings->screenShareFrameRate(), + 1, + nullptr); + GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + g_object_set(capsfilter, "caps", caps, nullptr); + gst_caps_unref(caps); + gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr); + + if (settings->screenSharePiP() && devices_.haveCamera()) { + GstElement *compositor = gst_element_factory_make("compositor", nullptr); + g_object_set(compositor, "background", 1, nullptr); + gst_bin_add(GST_BIN(pipe_), compositor); + if (!gst_element_link_many(ximagesrc, compositor, capsfilter, tee, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link screen share elements"); + return false; + } + GstPad *srcpad = gst_element_get_static_pad(camerafilter, "src"); + remotePiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); + if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, remotePiPSinkPad_))) { + nhlog::ui()->error("WebRTC: failed to link camerafilter -> compositor"); + gst_object_unref(srcpad); + return false; + } + gst_object_unref(srcpad); + } else if (!gst_element_link_many(ximagesrc, videoconvert, capsfilter, tee, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link screen share elements"); + return false; + } + } + + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr); + g_object_set(vp8enc, "deadline", 1, nullptr); + g_object_set(vp8enc, "error-resilient", 1, nullptr); + GstElement *rtpvp8pay = gst_element_factory_make("rtpvp8pay", nullptr); + GstElement *rtpqueue = gst_element_factory_make("queue", nullptr); + GstElement *rtpcapsfilter = gst_element_factory_make("capsfilter", nullptr); + GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", + "media", + G_TYPE_STRING, + "video", + "encoding-name", + G_TYPE_STRING, + "VP8", + "payload", + G_TYPE_INT, + vp8PayloadType, + nullptr); + g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr); + gst_caps_unref(rtpcaps); + + gst_bin_add_many(GST_BIN(pipe_), queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, nullptr); + + GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); + if (!gst_element_link_many( + tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link rtp video elements"); gst_object_unref(webrtcbin); - return true; + return false; + } + + if (callType_ == CallType::SCREEN && + !ChatPage::instance()->userSettings()->screenShareRemoteVideo()) { + GArray *transceivers; + g_signal_emit_by_name(webrtcbin, "get-transceivers", &transceivers); + GstWebRTCRTPTransceiver *transceiver = + g_array_index(transceivers, GstWebRTCRTPTransceiver *, 1); + transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; + g_array_unref(transceivers); + } + + gst_object_unref(webrtcbin); + return true; } bool WebRTCSession::haveLocalPiP() const { - if (state_ >= State::INITIATED) { - if (callType_ == CallType::VOICE || isRemoteVideoRecvOnly_) - return false; - else if (callType_ == CallType::SCREEN) - return true; - else { - GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee"); - if (tee) { - gst_object_unref(tee); - return true; - } - } + if (state_ >= State::INITIATED) { + if (callType_ == CallType::VOICE || isRemoteVideoRecvOnly_) + return false; + else if (callType_ == CallType::SCREEN) + return true; + else { + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee"); + if (tee) { + gst_object_unref(tee); + return true; + } } - return false; + } + return false; } bool WebRTCSession::isMicMuted() const { - if (state_ < State::INITIATED) - return false; + if (state_ < State::INITIATED) + return false; - GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); - gboolean muted; - g_object_get(srclevel, "mute", &muted, nullptr); - gst_object_unref(srclevel); - return muted; + GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); + gboolean muted; + g_object_get(srclevel, "mute", &muted, nullptr); + gst_object_unref(srclevel); + return muted; } bool WebRTCSession::toggleMicMute() { - if (state_ < State::INITIATED) - return false; + if (state_ < State::INITIATED) + return false; - GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); - gboolean muted; - g_object_get(srclevel, "mute", &muted, nullptr); - g_object_set(srclevel, "mute", !muted, nullptr); - gst_object_unref(srclevel); - return !muted; + GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); + gboolean muted; + g_object_get(srclevel, "mute", &muted, nullptr); + g_object_set(srclevel, "mute", !muted, nullptr); + gst_object_unref(srclevel); + return !muted; } void WebRTCSession::toggleLocalPiP() { - if (localPiPSinkPad_) { - guint zorder; - g_object_get(localPiPSinkPad_, "zorder", &zorder, nullptr); - g_object_set(localPiPSinkPad_, "zorder", zorder ? 0 : 2, nullptr); - } + if (localPiPSinkPad_) { + guint zorder; + g_object_get(localPiPSinkPad_, "zorder", &zorder, nullptr); + g_object_set(localPiPSinkPad_, "zorder", zorder ? 0 : 2, nullptr); + } } void WebRTCSession::clear() { - callType_ = webrtc::CallType::VOICE; - isOffering_ = false; - isRemoteVideoRecvOnly_ = false; - isRemoteVideoSendOnly_ = false; - videoItem_ = nullptr; - pipe_ = nullptr; - webrtc_ = nullptr; - busWatchId_ = 0; - shareWindowId_ = 0; - haveAudioStream_ = false; - haveVideoStream_ = false; - localPiPSinkPad_ = nullptr; - remotePiPSinkPad_ = nullptr; - localsdp_.clear(); - localcandidates_.clear(); + callType_ = webrtc::CallType::VOICE; + isOffering_ = false; + isRemoteVideoRecvOnly_ = false; + isRemoteVideoSendOnly_ = false; + videoItem_ = nullptr; + pipe_ = nullptr; + webrtc_ = nullptr; + busWatchId_ = 0; + shareWindowId_ = 0; + haveAudioStream_ = false; + haveVideoStream_ = false; + localPiPSinkPad_ = nullptr; + remotePiPSinkPad_ = nullptr; + localsdp_.clear(); + localcandidates_.clear(); } void WebRTCSession::end() { - nhlog::ui()->debug("WebRTC: ending session"); - keyFrameRequestData_ = KeyFrameRequestData{}; - if (pipe_) { - gst_element_set_state(pipe_, GST_STATE_NULL); - gst_object_unref(pipe_); - pipe_ = nullptr; - if (busWatchId_) { - g_source_remove(busWatchId_); - busWatchId_ = 0; - } + nhlog::ui()->debug("WebRTC: ending session"); + keyFrameRequestData_ = KeyFrameRequestData{}; + if (pipe_) { + gst_element_set_state(pipe_, GST_STATE_NULL); + gst_object_unref(pipe_); + pipe_ = nullptr; + if (busWatchId_) { + g_source_remove(busWatchId_); + busWatchId_ = 0; } + } - clear(); - if (state_ != State::DISCONNECTED) - emit stateChanged(State::DISCONNECTED); + clear(); + if (state_ != State::DISCONNECTED) + emit stateChanged(State::DISCONNECTED); } #else @@ -1135,13 +1105,13 @@ WebRTCSession::end() bool WebRTCSession::havePlugins(bool, std::string *) { - return false; + return false; } bool WebRTCSession::haveLocalPiP() const { - return false; + return false; } bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; } @@ -1149,13 +1119,13 @@ bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; } bool WebRTCSession::acceptOffer(const std::string &) { - return false; + return false; } bool WebRTCSession::acceptAnswer(const std::string &) { - return false; + return false; } void @@ -1165,13 +1135,13 @@ WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandi bool WebRTCSession::isMicMuted() const { - return false; + return false; } bool WebRTCSession::toggleMicMute() { - return false; + return false; } void