// SPDX-FileCopyrightText: Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include "CallDevices.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 frameRates; }; std::string name; GstDevice *device; std::vector caps; }; std::vector audioSources_; std::vector videoSources_; using FrameRate = std::pair; std::optional 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 &rates, const FrameRate &rate) { constexpr double minimumFrameRate = 15.0; if (static_cast(rate.first) / rate.second >= minimumFrameRate) rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); } void setDefaultDevice(bool isVideo) { auto settings = UserSettings::instance(); 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 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("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 std::vector deviceNames(T &sources, const std::string &defaultDevice) { std::vector 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 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 tokenise(std::string_view str, char delim) { std::pair ret; ret.first = std::atoi(str.data()); auto pos = str.find_first_of(delim); ret.second = std::atoi(str.data() + pos + 1); return ret; } } static GstDeviceMonitor *monitor = nullptr; void CallDevices::init() { 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; } } } void CallDevices::deinit() { if (monitor) { gst_device_monitor_stop(monitor); gst_object_unref(monitor); monitor = nullptr; } } bool CallDevices::haveMic() const { return !audioSources_.empty(); } bool CallDevices::haveCamera() const { return !videoSources_.empty(); } std::vector CallDevices::names(bool isVideo, const std::string &defaultDevice) const { return isVideo ? deviceNames(videoSources_, defaultDevice) : deviceNames(audioSources_, defaultDevice); } std::vector CallDevices::resolutions(const std::string &cameraName) const { std::vector 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 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 = UserSettings::instance()->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 &resolution, std::pair &frameRate) const { auto settings = UserSettings::instance(); 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 CallDevices::names(bool, const std::string &) const { return {}; } std::vector CallDevices::resolutions(const std::string &) const { return {}; } std::vector CallDevices::frameRates(const std::string &, const std::string &) const { return {}; } #endif