mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-26 13:08:48 +03:00
396 lines
13 KiB
C++
396 lines
13 KiB
C++
// 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
|