Allow choice of single window when sharing screen

This commit is contained in:
trilene 2021-02-24 17:07:01 -05:00
parent 70c77cdc44
commit efe240d609
6 changed files with 131 additions and 24 deletions

View file

@ -448,6 +448,7 @@ endif()
include(FindPkgConfig) include(FindPkgConfig)
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18) pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18)
if (TARGET PkgConfig::GSTREAMER) if (TARGET PkgConfig::GSTREAMER)
pkg_check_modules(XCB IMPORTED_TARGET xcb xcb-ewmh)
add_feature_info(voip ON "GStreamer found. Call support is enabled automatically.") add_feature_info(voip ON "GStreamer found. Call support is enabled automatically.")
else() else()
add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18 can be found via pkgconfig.") add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18 can be found via pkgconfig.")
@ -637,6 +638,10 @@ endif()
if (TARGET PkgConfig::GSTREAMER) if (TARGET PkgConfig::GSTREAMER)
target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER) target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER)
target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE) target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE)
if (TARGET PkgConfig::XCB)
target_link_libraries(nheko PRIVATE PkgConfig::XCB)
target_compile_definitions(nheko PRIVATE XCB_AVAILABLE)
endif()
endif() endif()
if(MSVC) if(MSVC)

View file

@ -13,9 +13,6 @@ Popup {
anchors.centerIn = parent; anchors.centerIn = parent;
frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate); frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate);
pipCheckBox.checked = Settings.screenSharePiP;
remoteVideoCheckBox.checked = Settings.screenShareRemoteVideo;
hideCursorCheckBox.checked = Settings.screenShareHideCursor;
} }
palette: colors palette: colors
@ -33,6 +30,27 @@ Popup {
RowLayout { RowLayout {
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
Layout.bottomMargin: 8
Label {
Layout.alignment: Qt.AlignLeft
text: qsTr("Window:")
color: colors.windowText
}
ComboBox {
id: windowCombo
Layout.fillWidth: true
model: CallManager.windowList()
}
}
RowLayout {
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 8
Label { Label {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
@ -43,7 +61,7 @@ Popup {
ComboBox { ComboBox {
id: frameRateCombo id: frameRateCombo
Layout.alignment: Qt.AlignRight Layout.fillWidth: true
model: ["25", "20", "15", "10", "5", "2", "1"] model: ["25", "20", "15", "10", "5", "2", "1"]
} }
@ -52,7 +70,8 @@ Popup {
CheckBox { CheckBox {
id: pipCheckBox id: pipCheckBox
visible: CallManager.cameras.length > 0 enabled: CallManager.cameras.length > 0
checked: Settings.screenSharePiP
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
@ -66,6 +85,7 @@ Popup {
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
text: qsTr("Request remote camera") text: qsTr("Request remote camera")
checked: Settings.screenShareRemoteVideo
ToolTip.text: qsTr("View your callee's camera like a regular video call") ToolTip.text: qsTr("View your callee's camera like a regular video call")
ToolTip.visible: hovered ToolTip.visible: hovered
} }
@ -76,7 +96,9 @@ Popup {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
Layout.bottomMargin: 8
text: qsTr("Hide mouse cursor") text: qsTr("Hide mouse cursor")
checked: Settings.screenShareHideCursor
} }
RowLayout { RowLayout {
@ -92,11 +114,14 @@ Popup {
onClicked: { onClicked: {
if (buttonLayout.validateMic()) { if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText; Settings.microphone = micCombo.currentText;
if (pipCheckBox.checked)
Settings.camera = cameraCombo.currentText;
Settings.screenShareFrameRate = frameRateCombo.currentText; Settings.screenShareFrameRate = frameRateCombo.currentText;
Settings.screenSharePiP = pipCheckBox.checked; Settings.screenSharePiP = pipCheckBox.checked;
Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked; Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
Settings.screenShareHideCursor = hideCursorCheckBox.checked; Settings.screenShareHideCursor = hideCursorCheckBox.checked;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN); CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex);
close(); close();
} }
} }

View file

@ -3,6 +3,7 @@
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <memory>
#include <QMediaPlaylist> #include <QMediaPlaylist>
#include <QUrl> #include <QUrl>
@ -18,6 +19,11 @@
#include "mtx/responses/turn_server.hpp" #include "mtx/responses/turn_server.hpp"
#ifdef XCB_AVAILABLE
#include <xcb/xcb.h>
#include <xcb/xcb_ewmh.h>
#endif
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>) Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate) Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
Q_DECLARE_METATYPE(mtx::responses::TurnServer) Q_DECLARE_METATYPE(mtx::responses::TurnServer)
@ -151,12 +157,18 @@ CallManager::CallManager(QObject *parent)
} }
void void
CallManager::sendInvite(const QString &roomid, CallType callType) CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
{ {
if (isOnCall()) if (isOnCall())
return; return;
if (callType == CallType::SCREEN && !screenShareSupported()) if (callType == CallType::SCREEN) {
return; if (!screenShareSupported())
return;
if (windows_.empty() || windowIndex >= windows_.size()) {
nhlog::ui()->error("WebRTC: window index out of range");
return;
}
}
auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
if (roomInfo.member_count != 2) { if (roomInfo.member_count != 2) {
@ -187,7 +199,7 @@ CallManager::sendInvite(const QString &roomid, CallType callType)
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
emit newInviteState(); emit newInviteState();
playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true); playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
if (!session_.createOffer(callType)) { if (!session_.createOffer(callType, windows_[windowIndex].second)) {
emit ChatPage::instance()->showNotification("Problem setting up call."); emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall(); endCall();
} }
@ -490,6 +502,69 @@ CallManager::stopRingtone()
player_.setPlaylist(nullptr); player_.setPlaylist(nullptr);
} }
QStringList
CallManager::windowList()
{
windows_.clear();
windows_.push_back({"Entire screen", 0});
#ifdef XCB_AVAILABLE
std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection(
xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); });
if (xcb_connection_has_error(connection.get())) {
nhlog::ui()->error("Failed to connect to X server");
return {};
}
xcb_ewmh_connection_t ewmh;
if (!xcb_ewmh_init_atoms_replies(
&ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) {
nhlog::ui()->error("Failed to connect to EWMH server");
return {};
}
std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>>
ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); });
for (int i = 0; i < ewmh.nb_screens; i++) {
xcb_ewmh_get_windows_reply_t clients;
if (!xcb_ewmh_get_client_list_reply(
&ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) {
nhlog::ui()->error("Failed to request window list");
return {};
}
for (uint32_t w = 0; w < clients.windows_len; w++) {
xcb_window_t window = clients.windows[w];
std::string name;
xcb_ewmh_get_utf8_strings_reply_t data;
auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) {
std::string name(r->strings, r->strings_len);
xcb_ewmh_get_utf8_strings_reply_wipe(r);
return name;
};
xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window);
if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr))
name = getName(&data);
cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window);
if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr))
name = getName(&data);
windows_.push_back({QString::fromStdString(name), window});
}
xcb_ewmh_get_windows_reply_wipe(&clients);
}
#endif
QStringList ret;
ret.reserve(windows_.size());
for (const auto &w : windows_)
ret.append(w.first);
return ret;
}
namespace { namespace {
std::vector<std::string> std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer) getTurnURIs(const mtx::responses::TurnServer &turnServer)

View file

@ -55,13 +55,14 @@ public:
static bool screenShareSupported(); static bool screenShareSupported();
public slots: public slots:
void sendInvite(const QString &roomid, webrtc::CallType); void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0);
void syncEvent(const mtx::events::collections::TimelineEvents &event); void syncEvent(const mtx::events::collections::TimelineEvents &event);
void toggleMicMute(); void toggleMicMute();
void toggleLocalPiP() { session_.toggleLocalPiP(); } void toggleLocalPiP() { session_.toggleLocalPiP(); }
void acceptInvite(); void acceptInvite();
void hangUp( void hangUp(
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
QStringList windowList();
signals: signals:
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
@ -91,6 +92,7 @@ private:
std::vector<std::string> turnURIs_; std::vector<std::string> turnURIs_;
QTimer turnServerTimer_; QTimer turnServerTimer_;
QMediaPlayer player_; QMediaPlayer player_;
std::vector<std::pair<QString, uint32_t>> windows_;
template<typename T> template<typename T>
bool handleEvent_(const mtx::events::collections::TimelineEvents &event); bool handleEvent_(const mtx::events::collections::TimelineEvents &event);

View file

@ -362,7 +362,7 @@ getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName)
} }
std::pair<int, int> std::pair<int, int>
getPiPDimensions(const std::pair<int, int> resolution, int fullWidth, double scaleFactor) getPiPDimensions(const std::pair<int, int> &resolution, int fullWidth, double scaleFactor)
{ {
int pipWidth = fullWidth * scaleFactor; int pipWidth = fullWidth * scaleFactor;
int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth; int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth;
@ -629,11 +629,12 @@ WebRTCSession::havePlugins(bool isVideo, std::string *errorMessage)
} }
bool bool
WebRTCSession::createOffer(CallType callType) WebRTCSession::createOffer(CallType callType, uint32_t shareWindowId)
{ {
clear(); clear();
isOffering_ = true; isOffering_ = true;
callType_ = callType; callType_ = callType;
shareWindowId_ = shareWindowId;
// opus and vp8 rtp payload types must be defined dynamically // opus and vp8 rtp payload types must be defined dynamically
// therefore from the range [96-127] // therefore from the range [96-127]
@ -888,15 +889,12 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType)
if (callType_ == CallType::VIDEO && !devices_.haveCamera()) if (callType_ == CallType::VIDEO && !devices_.haveCamera())
return !isOffering_; return !isOffering_;
auto settings = ChatPage::instance()->userSettings(); auto settings = ChatPage::instance()->userSettings();
if (callType_ == CallType::SCREEN && settings->screenSharePiP() && !devices_.haveCamera())
return false;
GstElement *camerafilter = nullptr; GstElement *camerafilter = nullptr;
GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
GstElement *tee = gst_element_factory_make("tee", "videosrctee"); GstElement *tee = gst_element_factory_make("tee", "videosrctee");
gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr); gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr);
if (callType_ == CallType::VIDEO || settings->screenSharePiP()) { if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) {
std::pair<int, int> resolution; std::pair<int, int> resolution;
std::pair<int, int> frameRate; std::pair<int, int> frameRate;
GstDevice *device = devices_.videoDevice(resolution, frameRate); GstDevice *device = devices_.videoDevice(resolution, frameRate);
@ -947,7 +945,7 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType)
return false; return false;
} }
g_object_set(ximagesrc, "use-damage", FALSE, nullptr); g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
g_object_set(ximagesrc, "xid", 0, nullptr); g_object_set(ximagesrc, "xid", shareWindowId_, nullptr);
g_object_set( g_object_set(
ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr); ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr);
@ -962,7 +960,7 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType)
gst_caps_unref(caps); gst_caps_unref(caps);
gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr); gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr);
if (settings->screenSharePiP()) { if (settings->screenSharePiP() && devices_.haveCamera()) {
GstElement *compositor = gst_element_factory_make("compositor", nullptr); GstElement *compositor = gst_element_factory_make("compositor", nullptr);
g_object_set(compositor, "background", 1, nullptr); g_object_set(compositor, "background", 1, nullptr);
gst_bin_add(GST_BIN(pipe_), compositor); gst_bin_add(GST_BIN(pipe_), compositor);
@ -1101,6 +1099,7 @@ WebRTCSession::clear()
pipe_ = nullptr; pipe_ = nullptr;
webrtc_ = nullptr; webrtc_ = nullptr;
busWatchId_ = 0; busWatchId_ = 0;
shareWindowId_ = 0;
haveAudioStream_ = false; haveAudioStream_ = false;
haveVideoStream_ = false; haveVideoStream_ = false;
localPiPSinkPad_ = nullptr; localPiPSinkPad_ = nullptr;
@ -1143,7 +1142,7 @@ WebRTCSession::haveLocalPiP() const
return false; return false;
} }
bool WebRTCSession::createOffer(webrtc::CallType) { return false; } bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; }
bool bool
WebRTCSession::acceptOffer(const std::string &) WebRTCSession::acceptOffer(const std::string &)

View file

@ -57,7 +57,7 @@ public:
bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; } bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; }
bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; } bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; }
bool createOffer(webrtc::CallType); bool createOffer(webrtc::CallType, uint32_t shareWindowId);
bool acceptOffer(const std::string &sdp); bool acceptOffer(const std::string &sdp);
bool acceptAnswer(const std::string &sdp); bool acceptAnswer(const std::string &sdp);
void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &); void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
@ -100,6 +100,7 @@ private:
GstElement *webrtc_ = nullptr; GstElement *webrtc_ = nullptr;
unsigned int busWatchId_ = 0; unsigned int busWatchId_ = 0;
std::vector<std::string> turnServers_; std::vector<std::string> turnServers_;
uint32_t shareWindowId_ = 0;
bool init(std::string *errorMessage = nullptr); bool init(std::string *errorMessage = nullptr);
bool startPipeline(int opusPayloadType, int vp8PayloadType); bool startPipeline(int opusPayloadType, int vp8PayloadType);