summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authortrilene <trilene@runbox.com>2020-08-05 17:56:44 -0400
committertrilene <trilene@runbox.com>2020-08-05 17:56:44 -0400
commitdf65093374c6c77f789e10b20144b3426c4358ac (patch)
tree9f0ca791eabc1adc2b931f5903707f31d8e8af0e /src
parentConditionally compile against upcoming GStreamer release (diff)
downloadnheko-df65093374c6c77f789e10b20144b3426c4358ac.tar.xz
Add audio input device selector
Diffstat (limited to 'src')
-rw-r--r--src/CallManager.cpp1
-rw-r--r--src/ChatPage.cpp1
-rw-r--r--src/UserSettingsPage.cpp21
-rw-r--r--src/UserSettingsPage.h7
-rw-r--r--src/WebRTCSession.cpp116
-rw-r--r--src/WebRTCSession.h7
-rw-r--r--src/dialogs/AcceptCall.cpp44
-rw-r--r--src/dialogs/AcceptCall.h7
-rw-r--r--src/dialogs/PlaceCall.cpp42
-rw-r--r--src/dialogs/PlaceCall.h7
10 files changed, 231 insertions, 22 deletions
diff --git a/src/CallManager.cpp b/src/CallManager.cpp
index 46781313..45890806 100644
--- a/src/CallManager.cpp
+++ b/src/CallManager.cpp
@@ -264,6 +264,7 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
                                               caller.display_name,
                                               QString::fromStdString(roomInfo.name),
                                               QString::fromStdString(roomInfo.avatar_url),
+                                              settings_,
                                               MainWindow::instance());
         connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() {
                 MainWindow::instance()->hideOverlay();
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 589aa3c7..84a5e4d3 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -474,6 +474,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                                   callee.display_name,
                                   QString::fromStdString(roomInfo.name),
                                   QString::fromStdString(roomInfo.avatar_url),
+                                  userSettings_,
                                   MainWindow::instance());
                                 connect(dialog, &dialogs::PlaceCall::voice, this, [this]() {
                                         callManager_.sendInvite(current_room_);
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index e67da997..ab5658a4 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -77,7 +77,8 @@ UserSettings::load()
         presence_ =
           settings.value("user/presence", QVariant::fromValue(Presence::AutomaticPresence))
             .value<Presence>();
-        useStunServer_ = settings.value("user/use_stun_server", false).toBool();
+        useStunServer_      = settings.value("user/use_stun_server", false).toBool();
+        defaultAudioSource_ = settings.value("user/default_audio_source", QString()).toString();
 
         applyTheme();
 }
@@ -291,6 +292,16 @@ UserSettings::setUseStunServer(bool useStunServer)
 }
 
 void
+UserSettings::setDefaultAudioSource(const QString &defaultAudioSource)
+{
+        if (defaultAudioSource == defaultAudioSource_)
+                return;
+        defaultAudioSource_ = defaultAudioSource;
+        emit defaultAudioSourceChanged(defaultAudioSource);
+        save();
+}
+
+void
 UserSettings::applyTheme()
 {
         QFile stylefile;
@@ -376,6 +387,7 @@ UserSettings::save()
         settings.setValue("emoji_font_family", emojiFont_);
         settings.setValue("presence", QVariant::fromValue(presence_));
         settings.setValue("use_stun_server", useStunServer_);
+        settings.setValue("default_audio_source", defaultAudioSource_);
 
         settings.endGroup();
 
@@ -501,6 +513,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
         callsLabel->setFont(font);
         useStunServer_ = new Toggle{this};
 
+        defaultAudioSourceValue_ = new QLabel(this);
+        defaultAudioSourceValue_->setFont(font);
+
         auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this};
         encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin);
         encryptionLabel_->setAlignment(Qt::AlignBottom);
@@ -634,9 +649,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
 
         formLayout_->addRow(callsLabel);
         formLayout_->addRow(new HorizontalLine{this});
-        boxWrap(tr("Allow Fallback Call Assist Server"),
+        boxWrap(tr("Allow fallback call assist server"),
                 useStunServer_,
                 tr("Will use turn.matrix.org as assist when your home server does not offer one."));
+        boxWrap(tr("Default audio source device"), defaultAudioSourceValue_);
 
         formLayout_->addRow(encryptionLabel_);
         formLayout_->addRow(new HorizontalLine{this});
@@ -797,6 +813,7 @@ UserSettingsPage::showEvent(QShowEvent *)
         deviceIdValue_->setText(QString::fromStdString(http::client()->device_id()));
         timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth());
         useStunServer_->setState(!settings_->useStunServer());
+        defaultAudioSourceValue_->setText(settings_->defaultAudioSource());
 
         deviceFingerprintValue_->setText(
           utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519));
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 567a7520..52ff9466 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -73,6 +73,8 @@ class UserSettings : public QObject
         Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged)
         Q_PROPERTY(
           bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
+        Q_PROPERTY(QString defaultAudioSource READ defaultAudioSource WRITE setDefaultAudioSource
+                     NOTIFY defaultAudioSourceChanged)
 
 public:
         UserSettings();
@@ -110,6 +112,7 @@ public:
         void setDecryptSidebar(bool state);
         void setPresence(Presence state);
         void setUseStunServer(bool state);
+        void setDefaultAudioSource(const QString &deviceName);
 
         QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
         bool messageHoverHighlight() const { return messageHoverHighlight_; }
@@ -136,6 +139,7 @@ public:
         QString emojiFont() const { return emojiFont_; }
         Presence presence() const { return presence_; }
         bool useStunServer() const { return useStunServer_; }
+        QString defaultAudioSource() const { return defaultAudioSource_; }
 
 signals:
         void groupViewStateChanged(bool state);
@@ -159,6 +163,7 @@ signals:
         void emojiFontChanged(QString state);
         void presenceChanged(Presence state);
         void useStunServerChanged(bool state);
+        void defaultAudioSourceChanged(const QString &deviceName);
 
 private:
         // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
@@ -187,6 +192,7 @@ private:
         QString emojiFont_;
         Presence presence_;
         bool useStunServer_;
+        QString defaultAudioSource_;
 };
 
 class HorizontalLine : public QFrame
@@ -244,6 +250,7 @@ private:
         Toggle *decryptSidebar_;
         QLabel *deviceFingerprintValue_;
         QLabel *deviceIdValue_;
+        QLabel *defaultAudioSourceValue_;
 
         QComboBox *themeCombo_;
         QComboBox *scaleFactorCombo_;
diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
index 07dfaac4..5638c607 100644
--- a/src/WebRTCSession.cpp
+++ b/src/WebRTCSession.cpp
@@ -487,23 +487,74 @@ WebRTCSession::startPipeline(int opusPayloadType)
         return true;
 }
 
-#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
-
 bool
 WebRTCSession::createPipeline(int opusPayloadType)
 {
-        std::string pipeline("webrtcbin bundle-policy=max-bundle name=webrtcbin "
-                             "autoaudiosrc ! volume name=srclevel ! audioconvert ! "
-                             "audioresample ! queue ! opusenc ! rtpopuspay ! "
-                             "queue ! " RTP_CAPS_OPUS +
-                             std::to_string(opusPayloadType) + " ! webrtcbin.");
+        int nSources = audioSources_ ? g_list_length(audioSources_) : 0;
+        if (nSources == 0) {
+                nhlog::ui()->error("WebRTC: no audio sources");
+                return false;
+        }
 
-        webrtc_       = nullptr;
-        GError *error = nullptr;
-        pipe_         = gst_parse_launch(pipeline.c_str(), &error);
-        if (error) {
-                nhlog::ui()->error("WebRTC: failed to parse pipeline: {}", error->message);
-                g_error_free(error);
+        if (audioSourceIndex_ < 0 || audioSourceIndex_ >= nSources) {
+                nhlog::ui()->error("WebRTC: invalid audio source index");
+                return false;
+        }
+
+        GstElement *source = gst_device_create_element(
+          GST_DEVICE_CAST(g_list_nth_data(audioSources_, audioSourceIndex_)), 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 pipeline elements");
                 end();
                 return false;
         }
@@ -541,3 +592,42 @@ WebRTCSession::end()
         if (state_ != State::DISCONNECTED)
                 emit stateChanged(State::DISCONNECTED);
 }
+
+void
+WebRTCSession::refreshDevices()
+{
+        if (!initialised_)
+                return;
+
+        static GstDeviceMonitor *monitor = nullptr;
+        if (!monitor) {
+                monitor       = gst_device_monitor_new();
+                GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
+                gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
+                gst_caps_unref(caps);
+        }
+        g_list_free_full(audioSources_, g_object_unref);
+        audioSources_ = gst_device_monitor_get_devices(monitor);
+}
+
+std::vector<std::string>
+WebRTCSession::getAudioSourceNames(const std::string &defaultDevice)
+{
+        if (!initialised_)
+                return {};
+
+        refreshDevices();
+        std::vector<std::string> ret;
+        ret.reserve(g_list_length(audioSources_));
+        for (GList *l = audioSources_; l != nullptr; l = l->next) {
+                gchar *name = gst_device_get_display_name(GST_DEVICE_CAST(l->data));
+                ret.emplace_back(name);
+                g_free(name);
+                if (ret.back() == defaultDevice) {
+                        // move default device to top of the list
+                        std::swap(audioSources_->data, l->data);
+                        std::swap(ret.front(), ret.back());
+                }
+        }
+        return ret;
+}
diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h
index 6b54f370..56d76fa8 100644
--- a/src/WebRTCSession.h
+++ b/src/WebRTCSession.h
@@ -7,6 +7,7 @@
 
 #include "mtx/events/voip.hpp"
 
+typedef struct _GList GList;
 typedef struct _GstElement GstElement;
 
 class WebRTCSession : public QObject
@@ -46,6 +47,9 @@ public:
         void setStunServer(const std::string &stunServer) { stunServer_ = stunServer; }
         void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; }
 
+        std::vector<std::string> getAudioSourceNames(const std::string &defaultDevice);
+        void setAudioSource(int audioDeviceIndex) { audioSourceIndex_ = audioDeviceIndex; }
+
 signals:
         void offerCreated(const std::string &sdp,
                           const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
@@ -66,9 +70,12 @@ private:
         GstElement *webrtc_ = nullptr;
         std::string stunServer_;
         std::vector<std::string> turnServers_;
+        GList *audioSources_  = nullptr;
+        int audioSourceIndex_ = -1;
 
         bool startPipeline(int opusPayloadType);
         bool createPipeline(int opusPayloadType);
+        void refreshDevices();
 
 public:
         WebRTCSession(WebRTCSession const &) = delete;
diff --git a/src/dialogs/AcceptCall.cpp b/src/dialogs/AcceptCall.cpp
index fd6565e2..be1eb0c9 100644
--- a/src/dialogs/AcceptCall.cpp
+++ b/src/dialogs/AcceptCall.cpp
@@ -1,11 +1,14 @@
+#include <QComboBox>
 #include <QLabel>
-#include <QPixmap>
 #include <QPushButton>
 #include <QString>
 #include <QVBoxLayout>
 
+#include "ChatPage.h"
 #include "Config.h"
+#include "UserSettingsPage.h"
 #include "Utils.h"
+#include "WebRTCSession.h"
 #include "dialogs/AcceptCall.h"
 #include "ui/Avatar.h"
 
@@ -15,9 +18,25 @@ AcceptCall::AcceptCall(const QString &caller,
                        const QString &displayName,
                        const QString &roomName,
                        const QString &avatarUrl,
+                       QSharedPointer<UserSettings> settings,
                        QWidget *parent)
   : QWidget(parent)
 {
+        std::string errorMessage;
+        if (!WebRTCSession::instance().init(&errorMessage)) {
+                emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
+                emit close();
+                return;
+        }
+        audioDevices_ = WebRTCSession::instance().getAudioSourceNames(
+          settings->defaultAudioSource().toStdString());
+        if (audioDevices_.empty()) {
+                emit ChatPage::instance()->showNotification(
+                  "Incoming call: No audio sources found.");
+                emit close();
+                return;
+        }
+
         setAutoFillBackground(true);
         setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
         setWindowModality(Qt::WindowModal);
@@ -55,7 +74,7 @@ AcceptCall::AcceptCall(const QString &caller,
         else
                 avatar->setLetter(utils::firstChar(roomName));
 
-        const int iconSize        = 24;
+        const int iconSize        = 22;
         QLabel *callTypeIndicator = new QLabel(this);
         callTypeIndicator->setPixmap(
           QIcon(":/icons/icons/ui/place-call.png").pixmap(QSize(iconSize * 2, iconSize * 2)));
@@ -66,7 +85,7 @@ AcceptCall::AcceptCall(const QString &caller,
         callTypeLabel->setAlignment(Qt::AlignCenter);
 
         auto buttonLayout = new QHBoxLayout;
-        buttonLayout->setSpacing(20);
+        buttonLayout->setSpacing(18);
         acceptBtn_ = new QPushButton(tr("Accept"), this);
         acceptBtn_->setDefault(true);
         acceptBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
@@ -78,6 +97,19 @@ AcceptCall::AcceptCall(const QString &caller,
         buttonLayout->addWidget(acceptBtn_);
         buttonLayout->addWidget(rejectBtn_);
 
+        auto deviceLayout = new QHBoxLayout;
+        auto audioLabel   = new QLabel(this);
+        audioLabel->setPixmap(
+          QIcon(":/icons/icons/ui/microphone-unmute.png").pixmap(QSize(iconSize, iconSize)));
+
+        auto deviceList = new QComboBox(this);
+        for (const auto &d : audioDevices_)
+                deviceList->addItem(QString::fromStdString(d));
+
+        deviceLayout->addStretch();
+        deviceLayout->addWidget(audioLabel);
+        deviceLayout->addWidget(deviceList);
+
         if (displayNameLabel)
                 layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
         layout->addWidget(callerLabel, 0, Qt::AlignCenter);
@@ -85,8 +117,12 @@ AcceptCall::AcceptCall(const QString &caller,
         layout->addWidget(callTypeIndicator, 0, Qt::AlignCenter);
         layout->addWidget(callTypeLabel, 0, Qt::AlignCenter);
         layout->addLayout(buttonLayout);
+        layout->addLayout(deviceLayout);
 
-        connect(acceptBtn_, &QPushButton::clicked, this, [this]() {
+        connect(acceptBtn_, &QPushButton::clicked, this, [this, deviceList, settings]() {
+                WebRTCSession::instance().setAudioSource(deviceList->currentIndex());
+                settings->setDefaultAudioSource(
+                  QString::fromStdString(audioDevices_[deviceList->currentIndex()]));
                 emit accept();
                 emit close();
         });
diff --git a/src/dialogs/AcceptCall.h b/src/dialogs/AcceptCall.h
index 5d2251fd..909605d0 100644
--- a/src/dialogs/AcceptCall.h
+++ b/src/dialogs/AcceptCall.h
@@ -1,9 +1,14 @@
 #pragma once
 
+#include <string>
+#include <vector>
+
+#include <QSharedPointer>
 #include <QWidget>
 
 class QPushButton;
 class QString;
+class UserSettings;
 
 namespace dialogs {
 
@@ -16,6 +21,7 @@ public:
                    const QString &displayName,
                    const QString &roomName,
                    const QString &avatarUrl,
+                   QSharedPointer<UserSettings> settings,
                    QWidget *parent = nullptr);
 
 signals:
@@ -25,6 +31,7 @@ signals:
 private:
         QPushButton *acceptBtn_;
         QPushButton *rejectBtn_;
+        std::vector<std::string> audioDevices_;
 };
 
 }
diff --git a/src/dialogs/PlaceCall.cpp b/src/dialogs/PlaceCall.cpp
index 0fda1794..4e70370a 100644
--- a/src/dialogs/PlaceCall.cpp
+++ b/src/dialogs/PlaceCall.cpp
@@ -1,10 +1,14 @@
+#include <QComboBox>
 #include <QLabel>
 #include <QPushButton>
 #include <QString>
 #include <QVBoxLayout>
 
+#include "ChatPage.h"
 #include "Config.h"
+#include "UserSettingsPage.h"
 #include "Utils.h"
+#include "WebRTCSession.h"
 #include "dialogs/PlaceCall.h"
 #include "ui/Avatar.h"
 
@@ -14,9 +18,24 @@ PlaceCall::PlaceCall(const QString &callee,
                      const QString &displayName,
                      const QString &roomName,
                      const QString &avatarUrl,
+                     QSharedPointer<UserSettings> settings,
                      QWidget *parent)
   : QWidget(parent)
 {
+        std::string errorMessage;
+        if (!WebRTCSession::instance().init(&errorMessage)) {
+                emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
+                emit close();
+                return;
+        }
+        audioDevices_ = WebRTCSession::instance().getAudioSourceNames(
+          settings->defaultAudioSource().toStdString());
+        if (audioDevices_.empty()) {
+                emit ChatPage::instance()->showNotification("No audio sources found.");
+                emit close();
+                return;
+        }
+
         setAutoFillBackground(true);
         setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
         setWindowModality(Qt::WindowModal);
@@ -37,25 +56,42 @@ PlaceCall::PlaceCall(const QString &callee,
                 avatar->setImage(avatarUrl);
         else
                 avatar->setLetter(utils::firstChar(roomName));
-        const int iconSize = 24;
+        const int iconSize = 18;
         voiceBtn_          = new QPushButton(tr("Voice"), this);
         voiceBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
         voiceBtn_->setIconSize(QSize(iconSize, iconSize));
         voiceBtn_->setDefault(true);
         cancelBtn_ = new QPushButton(tr("Cancel"), this);
 
-        buttonLayout->addStretch(1);
         buttonLayout->addWidget(avatar);
+        buttonLayout->addStretch();
         buttonLayout->addWidget(voiceBtn_);
         buttonLayout->addWidget(cancelBtn_);
 
         QString name  = displayName.isEmpty() ? callee : displayName;
         QLabel *label = new QLabel("Place a call to " + name + "?", this);
 
+        auto deviceLayout = new QHBoxLayout;
+        auto audioLabel   = new QLabel(this);
+        audioLabel->setPixmap(QIcon(":/icons/icons/ui/microphone-unmute.png")
+                                .pixmap(QSize(iconSize * 1.2, iconSize * 1.2)));
+
+        auto deviceList = new QComboBox(this);
+        for (const auto &d : audioDevices_)
+                deviceList->addItem(QString::fromStdString(d));
+
+        deviceLayout->addStretch();
+        deviceLayout->addWidget(audioLabel);
+        deviceLayout->addWidget(deviceList);
+
         layout->addWidget(label);
         layout->addLayout(buttonLayout);
+        layout->addLayout(deviceLayout);
 
-        connect(voiceBtn_, &QPushButton::clicked, this, [this]() {
+        connect(voiceBtn_, &QPushButton::clicked, this, [this, deviceList, settings]() {
+                WebRTCSession::instance().setAudioSource(deviceList->currentIndex());
+                settings->setDefaultAudioSource(
+                  QString::fromStdString(audioDevices_[deviceList->currentIndex()]));
                 emit voice();
                 emit close();
         });
diff --git a/src/dialogs/PlaceCall.h b/src/dialogs/PlaceCall.h
index f6db9ab5..5a1e982c 100644
--- a/src/dialogs/PlaceCall.h
+++ b/src/dialogs/PlaceCall.h
@@ -1,9 +1,14 @@
 #pragma once
 
+#include <string>
+#include <vector>
+
+#include <QSharedPointer>
 #include <QWidget>
 
 class QPushButton;
 class QString;
+class UserSettings;
 
 namespace dialogs {
 
@@ -16,6 +21,7 @@ public:
                   const QString &displayName,
                   const QString &roomName,
                   const QString &avatarUrl,
+                  QSharedPointer<UserSettings> settings,
                   QWidget *parent = nullptr);
 
 signals:
@@ -25,6 +31,7 @@ signals:
 private:
         QPushButton *voiceBtn_;
         QPushButton *cancelBtn_;
+        std::vector<std::string> audioDevices_;
 };
 
 }