diff --git a/resources/icons/ui/toggle-camera-view.png b/resources/icons/ui/toggle-camera-view.png new file mode 100644 index 00000000..a1a6a513 Binary files /dev/null and b/resources/icons/ui/toggle-camera-view.png differ diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index 7137197b..2a83a8e1 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -103,6 +103,22 @@ Rectangle { Layout.fillWidth: true } + ImageButton { + visible: TimelineManager.onVideoCall + width: 24 + height: 24 + buttonTextColor: "#000000" + image: ":/icons/icons/ui/toggle-camera-view.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: "Toggle camera view" + onClicked: TimelineManager.toggleCameraView() + } + + Item { + implicitWidth: 8 + } + ImageButton { width: 24 height: 24 diff --git a/resources/res.qrc b/resources/res.qrc index 4eba0bca..efb9c907 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -74,6 +74,7 @@ icons/ui/end-call.png icons/ui/microphone-mute.png icons/ui/microphone-unmute.png + icons/ui/toggle-camera-view.png icons/ui/video-call.png icons/emoji-categories/people.png diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp index 31d5b5ed..778a97d8 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp @@ -103,6 +103,7 @@ bool haveAudioStream_; bool haveVideoStream_; std::vector audioSources_; std::vector videoSources_; +GstPad *insetSinkPad_ = nullptr; using FrameRate = std::pair; std::optional @@ -496,6 +497,92 @@ setWaitForKeyFrame(GstBin *decodebin G_GNUC_UNUSED, GstElement *element, gpointe } #endif +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 * +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(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 +getResolution(GstPad *pad) +{ + std::pair 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; +} + +void +addCameraView(GstElement *pipe, const std::pair &videoCallSize) +{ + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *videorate = gst_element_factory_make("videorate", nullptr); + gst_bin_add_many(GST_BIN(pipe), queue, videorate, nullptr); + gst_element_link_many(tee, queue, videorate, nullptr); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(videorate); + gst_object_unref(tee); + + GstElement *camerafilter = gst_bin_get_by_name(GST_BIN(pipe), "camerafilter"); + GstPad *filtersinkpad = gst_element_get_static_pad(camerafilter, "sink"); + auto cameraResolution = getResolution(filtersinkpad); + int insetWidth = videoCallSize.first / 4; + int insetHeight = + static_cast(cameraResolution.second) / cameraResolution.first * insetWidth; + nhlog::ui()->debug("WebRTC: picture-in-picture size: {}x{}", insetWidth, insetHeight); + gst_object_unref(filtersinkpad); + gst_object_unref(camerafilter); + + GstPad *camerapad = gst_element_get_static_pad(videorate, "src"); + GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor"); + insetSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); + g_object_set(insetSinkPad_, "zorder", 2, nullptr); + g_object_set(insetSinkPad_, "width", insetWidth, "height", insetHeight, nullptr); + gint offset = videoCallSize.first / 80; + g_object_set(insetSinkPad_, "xpos", offset, "ypos", offset, nullptr); + if (GST_PAD_LINK_FAILED(gst_pad_link(camerapad, insetSinkPad_))) + nhlog::ui()->error("WebRTC: failed to link camera view chain"); + gst_object_unref(camerapad); + gst_object_unref(compositor); +} + void linkNewPad(GstElement *decodebin, GstPad *newpad, GstElement *pipe) { @@ -511,51 +598,29 @@ linkNewPad(GstElement *decodebin, GstPad *newpad, GstElement *pipe) gst_object_unref(sinkpad); WebRTCSession *session = &WebRTCSession::instance(); - GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *queue = nullptr; if (!std::strcmp(mediaType, "audio")) { nhlog::ui()->debug("WebRTC: received incoming audio stream"); - haveAudioStream_ = true; - 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); + 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); - gst_object_unref(queue); nhlog::ui()->error("WebRTC: video call item not set"); return; } haveVideoStream_ = true; keyFrameRequestData_.statsField = std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc); - GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); - 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(qmlglsink, "widget", session->getVideoItem(), nullptr); - g_object_set(glsinkbin, "sink", qmlglsink, nullptr); - - gst_bin_add_many( - GST_BIN(pipe), queue, videoconvert, glupload, glcolorconvert, glsinkbin, nullptr); - gst_element_link_many( - queue, videoconvert, glupload, glcolorconvert, glsinkbin, nullptr); - gst_element_sync_state_with_parent(queue); - gst_element_sync_state_with_parent(videoconvert); - gst_element_sync_state_with_parent(glupload); - gst_element_sync_state_with_parent(glcolorconvert); - gst_element_sync_state_with_parent(glsinkbin); + queue = newVideoSinkChain(pipe); + auto videoCallSize = getResolution(newpad); + nhlog::ui()->info("WebRTC: incoming video resolution: {}x{}", + videoCallSize.first, + videoCallSize.second); + addCameraView(pipe, videoCallSize); } else { g_free(mediaType); - gst_object_unref(queue); nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad)); return; } @@ -600,7 +665,7 @@ addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe) 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 new pad"); + nhlog::ui()->error("WebRTC: unable to link decodebin"); gst_object_unref(sinkpad); } @@ -689,7 +754,8 @@ WebRTCSession::havePlugins(bool isVideo, std::string *errorMessage) "webrtc", nullptr}; - const gchar *videoPlugins[] = {"opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr}; + const gchar *videoPlugins[] = { + "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr}; std::string strError("Missing GStreamer plugins: "); const gchar **needed = isVideo ? videoPlugins : voicePlugins; @@ -729,6 +795,7 @@ WebRTCSession::createOffer(bool isVideo) videoItem_ = nullptr; haveAudioStream_ = false; haveVideoStream_ = false; + insetSinkPad_ = nullptr; localsdp_.clear(); localcandidates_.clear(); @@ -752,6 +819,7 @@ WebRTCSession::acceptOffer(const std::string &sdp) videoItem_ = nullptr; haveAudioStream_ = false; haveVideoStream_ = false; + insetSinkPad_ = nullptr; localsdp_.clear(); localcandidates_.clear(); @@ -974,6 +1042,7 @@ WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType) nhlog::ui()->error("WebRTC: failed to link audio pipeline elements"); return false; } + return isVideo_ ? addVideoPipeline(vp8PayloadType) : true; } @@ -984,8 +1053,9 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType) if (videoSources_.empty()) return !isOffering_; - std::string cameraSetting = ChatPage::instance()->userSettings()->camera().toStdString(); - auto it = std::find_if(videoSources_.cbegin(), + QSharedPointer settings = ChatPage::instance()->userSettings(); + std::string cameraSetting = settings->camera().toStdString(); + auto it = std::find_if(videoSources_.cbegin(), videoSources_.cend(), [&cameraSetting](const auto &s) { return s.name == cameraSetting; }); if (it == videoSources_.cend()) { @@ -993,11 +1063,9 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType) return false; } - std::string resSetting = - ChatPage::instance()->userSettings()->cameraResolution().toStdString(); + std::string resSetting = settings->cameraResolution().toStdString(); const std::string &res = resSetting.empty() ? it->caps.front().resolution : resSetting; - std::string frSetting = - ChatPage::instance()->userSettings()->cameraFrameRate().toStdString(); + std::string frSetting = settings->cameraFrameRate().toStdString(); const std::string &fr = frSetting.empty() ? it->caps.front().frameRates.front() : frSetting; auto resolution = tokenise(res, 'x'); auto frameRate = tokenise(fr, '/'); @@ -1005,9 +1073,10 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType) nhlog::ui()->debug("WebRTC: camera resolution: {}x{}", resolution.first, resolution.second); nhlog::ui()->debug("WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second); - GstElement *source = gst_device_create_element(it->device, nullptr); - GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); - GstCaps *caps = gst_caps_new_simple("video/x-raw", + GstElement *source = gst_device_create_element(it->device, nullptr); + GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); + GstElement *capsfilter = gst_element_factory_make("capsfilter", "camerafilter"); + GstCaps *caps = gst_caps_new_simple("video/x-raw", "width", G_TYPE_INT, resolution.first, @@ -1021,15 +1090,13 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType) nullptr); g_object_set(capsfilter, "caps", caps, nullptr); gst_caps_unref(caps); - - GstElement *convert = gst_element_factory_make("videoconvert", nullptr); - GstElement *queue1 = gst_element_factory_make("queue", nullptr); - GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr); + GstElement *tee = gst_element_factory_make("tee", "videosrctee"); + 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 *rtp = gst_element_factory_make("rtpvp8pay", nullptr); - GstElement *queue2 = gst_element_factory_make("queue", 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", @@ -1047,27 +1114,30 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType) gst_bin_add_many(GST_BIN(pipe_), source, + videoconvert, capsfilter, - convert, - queue1, + tee, + queue, vp8enc, - rtp, - queue2, + rtpvp8pay, + rtpqueue, rtpcapsfilter, nullptr); GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); if (!gst_element_link_many(source, + videoconvert, capsfilter, - convert, - queue1, + tee, + queue, vp8enc, - rtp, - queue2, + rtpvp8pay, + rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) { nhlog::ui()->error("WebRTC: failed to link video pipeline elements"); + gst_object_unref(webrtcbin); return false; } gst_object_unref(webrtcbin); @@ -1101,6 +1171,16 @@ WebRTCSession::toggleMicMute() return !muted; } +void +WebRTCSession::toggleCameraView() +{ + if (insetSinkPad_) { + guint zorder; + g_object_get(insetSinkPad_, "zorder", &zorder, nullptr); + g_object_set(insetSinkPad_, "zorder", zorder ? 0 : 2, nullptr); + } +} + void WebRTCSession::end() { @@ -1115,11 +1195,13 @@ WebRTCSession::end() busWatchId_ = 0; } } + webrtc_ = nullptr; isVideo_ = false; isOffering_ = false; isRemoteVideoRecvOnly_ = false; videoItem_ = nullptr; + insetSinkPad_ = nullptr; if (state_ != State::DISCONNECTED) emit stateChanged(State::DISCONNECTED); } @@ -1270,6 +1352,10 @@ WebRTCSession::toggleMicMute() return false; } +void +WebRTCSession::toggleCameraView() +{} + void WebRTCSession::end() {} diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h index 9c7778e7..57002f8f 100644 --- a/src/WebRTCSession.h +++ b/src/WebRTCSession.h @@ -53,6 +53,7 @@ public: bool isMicMuted() const; bool toggleMicMute(); + void toggleCameraView(); void end(); void setTurnServers(const std::vector &uris) { turnServers_ = uris; } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index f9d7d00c..3146c92f 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -330,6 +330,12 @@ TimelineViewManager::toggleMicMute() emit micMuteChanged(); } +void +TimelineViewManager::toggleCameraView() +{ + WebRTCSession::instance().toggleCameraView(); +} + void TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 67eeee5b..f330d870 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -61,6 +61,7 @@ public: QString callPartyAvatarUrl() const { return callManager_->callPartyAvatarUrl(); } bool isMicMuted() const { return WebRTCSession::instance().isMicMuted(); } Q_INVOKABLE void toggleMicMute(); + Q_INVOKABLE void toggleCameraView(); Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const;