diff options
Diffstat (limited to 'src/voip/CallDevices.cpp')
-rw-r--r-- | src/voip/CallDevices.cpp | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/src/voip/CallDevices.cpp b/src/voip/CallDevices.cpp new file mode 100644 index 00000000..be185470 --- /dev/null +++ b/src/voip/CallDevices.cpp @@ -0,0 +1,385 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <cstring> +#include <optional> +#include <string_view> + +#include "CallDevices.h" +#include "ChatPage.h" +#include "Logging.h" +#include "UserSettingsPage.h" + +#ifdef GSTREAMER_AVAILABLE +extern "C" +{ +#include "gst/gst.h" +} +#endif + +CallDevices::CallDevices() + : QObject() +{} + +#ifdef GSTREAMER_AVAILABLE +namespace { + +struct AudioSource +{ + std::string name; + GstDevice *device; +}; + +struct VideoSource +{ + struct Caps + { + std::string resolution; + std::vector<std::string> frameRates; + }; + std::string name; + GstDevice *device; + std::vector<Caps> caps; +}; + +std::vector<AudioSource> audioSources_; +std::vector<VideoSource> videoSources_; + +using FrameRate = std::pair<int, int>; +std::optional<FrameRate> +getFrameRate(const GValue *value) +{ + if (GST_VALUE_HOLDS_FRACTION(value)) { + gint num = gst_value_get_fraction_numerator(value); + gint den = gst_value_get_fraction_denominator(value); + return FrameRate{num, den}; + } + return std::nullopt; +} + +void +addFrameRate(std::vector<std::string> &rates, const FrameRate &rate) +{ + constexpr double minimumFrameRate = 15.0; + if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate) + rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); +} + +void +setDefaultDevice(bool isVideo) +{ + auto settings = ChatPage::instance()->userSettings(); + if (isVideo && settings->camera().isEmpty()) { + const VideoSource &camera = videoSources_.front(); + settings->setCamera(QString::fromStdString(camera.name)); + settings->setCameraResolution(QString::fromStdString(camera.caps.front().resolution)); + settings->setCameraFrameRate( + QString::fromStdString(camera.caps.front().frameRates.front())); + } else if (!isVideo && settings->microphone().isEmpty()) { + settings->setMicrophone(QString::fromStdString(audioSources_.front().name)); + } +} + +void +addDevice(GstDevice *device) +{ + if (!device) + return; + + gchar *name = gst_device_get_display_name(device); + gchar *type = gst_device_get_device_class(device); + bool isVideo = !std::strncmp(type, "Video", 5); + g_free(type); + nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name); + if (!isVideo) { + audioSources_.push_back({name, device}); + g_free(name); + setDefaultDevice(false); + return; + } + + GstCaps *gstcaps = gst_device_get_caps(device); + if (!gstcaps) { + nhlog::ui()->debug("WebRTC: unable to get caps for {}", name); + g_free(name); + return; + } + + VideoSource source{name, device, {}}; + g_free(name); + guint nCaps = gst_caps_get_size(gstcaps); + for (guint i = 0; i < nCaps; ++i) { + GstStructure *structure = gst_caps_get_structure(gstcaps, i); + const gchar *struct_name = gst_structure_get_name(structure); + if (!std::strcmp(struct_name, "video/x-raw")) { + gint widthpx, heightpx; + if (gst_structure_get(structure, + "width", + G_TYPE_INT, + &widthpx, + "height", + G_TYPE_INT, + &heightpx, + nullptr)) { + VideoSource::Caps caps; + caps.resolution = std::to_string(widthpx) + "x" + std::to_string(heightpx); + const GValue *value = gst_structure_get_value(structure, "framerate"); + if (auto fr = getFrameRate(value); fr) + addFrameRate(caps.frameRates, *fr); + else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) { + addFrameRate(caps.frameRates, + *getFrameRate(gst_value_get_fraction_range_min(value))); + addFrameRate(caps.frameRates, + *getFrameRate(gst_value_get_fraction_range_max(value))); + } else if (GST_VALUE_HOLDS_LIST(value)) { + guint nRates = gst_value_list_get_size(value); + for (guint j = 0; j < nRates; ++j) { + const GValue *rate = gst_value_list_get_value(value, j); + if (auto frate = getFrameRate(rate); frate) + addFrameRate(caps.frameRates, *frate); + } + } + if (!caps.frameRates.empty()) + source.caps.push_back(std::move(caps)); + } + } + } + gst_caps_unref(gstcaps); + videoSources_.push_back(std::move(source)); + setDefaultDevice(true); +} + +template<typename T> +bool +removeDevice(T &sources, GstDevice *device, bool changed) +{ + if (auto it = std::find_if( + sources.begin(), sources.end(), [device](const auto &s) { return s.device == device; }); + it != sources.end()) { + nhlog::ui()->debug( + std::string("WebRTC: device ") + (changed ? "changed: " : "removed: ") + "{}", it->name); + gst_object_unref(device); + sources.erase(it); + return true; + } + return false; +} + +void +removeDevice(GstDevice *device, bool changed) +{ + if (device) { + if (removeDevice(audioSources_, device, changed) || + removeDevice(videoSources_, device, changed)) + return; + } +} + +gboolean +newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED) +{ + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_DEVICE_ADDED: { + GstDevice *device; + gst_message_parse_device_added(msg, &device); + addDevice(device); + emit CallDevices::instance().devicesChanged(); + break; + } + case GST_MESSAGE_DEVICE_REMOVED: { + GstDevice *device; + gst_message_parse_device_removed(msg, &device); + removeDevice(device, false); + emit CallDevices::instance().devicesChanged(); + break; + } + case GST_MESSAGE_DEVICE_CHANGED: { + GstDevice *device; + GstDevice *oldDevice; + gst_message_parse_device_changed(msg, &device, &oldDevice); + removeDevice(oldDevice, true); + addDevice(device); + break; + } + default: + break; + } + return TRUE; +} + +template<typename T> +std::vector<std::string> +deviceNames(T &sources, const std::string &defaultDevice) +{ + std::vector<std::string> ret; + ret.reserve(sources.size()); + for (const auto &s : sources) + ret.push_back(s.name); + + // move default device to top of the list + if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end()) + std::swap(ret.front(), *it); + + return ret; +} + +std::optional<VideoSource> +getVideoSource(const std::string &cameraName) +{ + if (auto it = std::find_if(videoSources_.cbegin(), + videoSources_.cend(), + [&cameraName](const auto &s) { return s.name == cameraName; }); + it != videoSources_.cend()) { + return *it; + } + return std::nullopt; +} + +std::pair<int, int> +tokenise(std::string_view str, char delim) +{ + std::pair<int, int> ret; + ret.first = std::atoi(str.data()); + auto pos = str.find_first_of(delim); + ret.second = std::atoi(str.data() + pos + 1); + return ret; +} +} + +void +CallDevices::init() +{ + 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_device_monitor_add_filter(monitor, "Audio/Duplex", caps); + gst_caps_unref(caps); + caps = gst_caps_new_empty_simple("video/x-raw"); + gst_device_monitor_add_filter(monitor, "Video/Source", caps); + gst_device_monitor_add_filter(monitor, "Video/Duplex", caps); + gst_caps_unref(caps); + + GstBus *bus = gst_device_monitor_get_bus(monitor); + gst_bus_add_watch(bus, newBusMessage, nullptr); + gst_object_unref(bus); + if (!gst_device_monitor_start(monitor)) { + nhlog::ui()->error("WebRTC: failed to start device monitor"); + return; + } + } +} + +bool +CallDevices::haveMic() const +{ + return !audioSources_.empty(); +} + +bool +CallDevices::haveCamera() const +{ + return !videoSources_.empty(); +} + +std::vector<std::string> +CallDevices::names(bool isVideo, const std::string &defaultDevice) const +{ + return isVideo ? deviceNames(videoSources_, defaultDevice) + : deviceNames(audioSources_, defaultDevice); +} + +std::vector<std::string> +CallDevices::resolutions(const std::string &cameraName) const +{ + std::vector<std::string> ret; + if (auto s = getVideoSource(cameraName); s) { + ret.reserve(s->caps.size()); + for (const auto &c : s->caps) + ret.push_back(c.resolution); + } + return ret; +} + +std::vector<std::string> +CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const +{ + if (auto s = getVideoSource(cameraName); s) { + if (auto it = std::find_if(s->caps.cbegin(), + s->caps.cend(), + [&](const auto &c) { return c.resolution == resolution; }); + it != s->caps.cend()) + return it->frameRates; + } + return {}; +} + +GstDevice * +CallDevices::audioDevice() const +{ + std::string name = ChatPage::instance()->userSettings()->microphone().toStdString(); + if (auto it = std::find_if(audioSources_.cbegin(), + audioSources_.cend(), + [&name](const auto &s) { return s.name == name; }); + it != audioSources_.cend()) { + nhlog::ui()->debug("WebRTC: microphone: {}", name); + return it->device; + } else { + nhlog::ui()->error("WebRTC: unknown microphone: {}", name); + return nullptr; + } +} + +GstDevice * +CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const +{ + auto settings = ChatPage::instance()->userSettings(); + std::string name = settings->camera().toStdString(); + if (auto s = getVideoSource(name); s) { + nhlog::ui()->debug("WebRTC: camera: {}", name); + resolution = tokenise(settings->cameraResolution().toStdString(), 'x'); + frameRate = tokenise(settings->cameraFrameRate().toStdString(), '/'); + nhlog::ui()->debug("WebRTC: camera resolution: {}x{}", resolution.first, resolution.second); + nhlog::ui()->debug("WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second); + return s->device; + } else { + nhlog::ui()->error("WebRTC: unknown camera: {}", name); + return nullptr; + } +} + +#else + +bool +CallDevices::haveMic() const +{ + return false; +} + +bool +CallDevices::haveCamera() const +{ + return false; +} + +std::vector<std::string> +CallDevices::names(bool, const std::string &) const +{ + return {}; +} + +std::vector<std::string> +CallDevices::resolutions(const std::string &) const +{ + return {}; +} + +std::vector<std::string> +CallDevices::frameRates(const std::string &, const std::string &) const +{ + return {}; +} + +#endif |