Files
vimix/DeviceSource.cpp
Bruno Herbelin 452221daa5 User input unified and fixed for clone source
Fixed slider in player, show filtered image when disabled (outside mixing circle), correct timing for clone source (different for filters).
2022-06-20 17:29:12 +02:00

713 lines
23 KiB
C++

/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
#include <gst/pbutils/gstdiscoverer.h>
#include <gst/pbutils/pbutils.h>
#include <gst/gst.h>
#include "defines.h"
#include "Log.h"
#include "ImageShader.h"
#include "ImageProcessingShader.h"
#include "Resource.h"
#include "Decorations.h"
#include "Stream.h"
#include "Visitor.h"
#include "CloneSource.h"
#include "DeviceSource.h"
#ifndef NDEBUG
#define DEVICE_DEBUG
#define GST_DEVICE_DEBUG
#endif
#if defined(APPLE)
std::string gst_plugin_device = "avfvideosrc";
std::string gst_plugin_vidcap = "avfvideosrc capture-screen=true";
#else
std::string gst_plugin_device = "v4l2src";
std::string gst_plugin_vidcap = "ximagesrc";
#endif
std::string pipelineForDevice(GstDevice *device, uint index)
{
std::ostringstream pipe;
GstStructure *stru = gst_device_get_properties(device);
if (stru) {
const gchar *api = gst_structure_get_string(stru, "device.api");
// check that the api is supported by vimix
if (api && gst_plugin_device.find(api) != std::string::npos)
{
pipe << gst_plugin_device;
#if defined(APPLE)
pipe << " device-index=" << index;
#else
(void) index; // prevent compilatin warning unused argument
// get the path: either "device.path" or "api.v4l2.path"
// (GStreamer has different ways depending on version)
const gchar *path;
if ( gst_structure_has_field (stru, "device.path"))
path = gst_structure_get_string(stru, "device.path");
else
path = gst_structure_get_string(stru, "api.v4l2.path");
pipe << " device=" << path;
#endif
}
}
return pipe.str();
}
gboolean
Device::callback_device_monitor (GstBus *, GstMessage * message, gpointer )
{
GstDevice *device;
switch (GST_MESSAGE_TYPE (message)) {
case GST_MESSAGE_DEVICE_ADDED:
{
gst_message_parse_device_added (message, &device);
manager().add(device);
gst_object_unref (device);
}
break;
case GST_MESSAGE_DEVICE_REMOVED:
{
gst_message_parse_device_removed (message, &device);
manager().remove(device);
gst_object_unref (device);
}
break;
default:
break;
}
return G_SOURCE_CONTINUE;
}
struct hasDeviceName: public std::unary_function<DeviceHandle, bool>
{
inline bool operator()(const DeviceHandle &elem) const {
return (elem.name.compare(_name) == 0);
}
explicit hasDeviceName(const std::string &name) : _name(name) { }
private:
std::string _name;
};
struct hasConnectedSource: public std::unary_function<DeviceHandle, bool>
{
inline bool operator()(const DeviceHandle &elem) const {
auto sit = std::find(elem.connected_sources.begin(), elem.connected_sources.end(), s_);
return sit != elem.connected_sources.end();
}
explicit hasConnectedSource(Source *s) : s_(s) { }
private:
Source *s_;
};
void Device::add(GstDevice *device)
{
if (device==nullptr)
return;
gchar *device_name = gst_device_get_display_name (device);
// lock before change
access_.lock();
// if device with this name is not already listed
auto handle = std::find_if(handles_.cbegin(), handles_.cend(), hasDeviceName(device_name) );
if ( handle == handles_.cend() ) {
std::string p = pipelineForDevice(device, handles_.size());
DeviceConfigSet confs = getDeviceConfigs(p);
// add if not in the list and valid
if (!p.empty() && !confs.empty()) {
DeviceHandle dev;
dev.name = device_name;
dev.pipeline = p;
dev.configs = confs;
handles_.push_back(dev);
#ifdef GST_DEVICE_DEBUG
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
g_print("\nDevice %s plugged : %s\n", device_name, stru);
g_free (stru);
#endif
}
}
// unlock access
access_.unlock();
g_free (device_name);
}
void Device::remove(GstDevice *device)
{
if (device==nullptr)
return;
gchar *device_name = gst_device_get_display_name (device);
// lock before change
access_.lock();
// if a device with this name is listed
auto handle = std::find_if(handles_.cbegin(), handles_.cend(), hasDeviceName(device_name) );
if ( handle != handles_.cend() ) {
// remove the handle if there is no source connected
if (handle->connected_sources.empty())
handles_.erase(handle);
else {
// otherwise unplug all sources
for (auto sit = handle->connected_sources.begin(); sit != handle->connected_sources.end(); ++sit)
(*sit)->unplug();
// and inform user
Log::Warning("Device %s unplugged.", device_name);
// NB; the handle will be removed at the destruction of the last DeviceSource
}
}
// unlock access
access_.unlock();
g_free (device_name);
}
Device::Device(): monitor_initialized_(false), monitor_unplug_event_(false)
{
std::thread(launchMonitoring, this).detach();
}
void Device::launchMonitoring(Device *d)
{
// gstreamer monitoring of devices
d->monitor_ = gst_device_monitor_new ();
// watching all video stream sources
GstCaps *caps = gst_caps_new_empty_simple ("video/x-raw");
gst_device_monitor_add_filter (d->monitor_, "Video/Source", caps);
gst_caps_unref (caps);
gst_device_monitor_set_show_all_devices(d->monitor_, true);
gst_device_monitor_start (d->monitor_);
// Add configs for already plugged devices
GList *devices = gst_device_monitor_get_devices(d->monitor_);
GList *tmp;
for (tmp = devices; tmp ; tmp = tmp->next ) {
GstDevice *device = (GstDevice *) tmp->data;
d->add(device);
gst_object_unref (device);
}
g_list_free(devices);
// Try to get captrure screen
DeviceConfigSet confs = getDeviceConfigs(gst_plugin_vidcap);
if (!confs.empty()) {
DeviceConfig best = *confs.rbegin();
DeviceConfigSet confscreen;
// limit framerate to 30fps
best.fps_numerator = MIN( best.fps_numerator, 30);
best.fps_denominator = 1;
confscreen.insert(best);
// add to config list
d->access_.lock();
DeviceHandle dev;
dev.name = "Screen capture";
dev.pipeline = gst_plugin_vidcap;
dev.configs = confscreen;
d->handles_.push_back(dev);
d->access_.unlock();
}
// monitor is initialized
d->monitor_initialized_ = true;
d->monitor_initialization_.notify_all();
// create a local g_main_context to launch monitoring in this thread
GMainContext *_gcontext_device = g_main_context_new();
g_main_context_acquire(_gcontext_device);
// temporarily push as default the default g_main_context for add_watch
g_main_context_push_thread_default(_gcontext_device);
// add a bus watch on the device monitoring using the main loop we created
GstBus *bus = gst_device_monitor_get_bus (d->monitor_);
gst_bus_add_watch (bus, callback_device_monitor, NULL);
gst_object_unref (bus);
// restore g_main_context
g_main_context_pop_thread_default(_gcontext_device);
// start main loop for this context (blocking infinitely)
g_main_loop_run( g_main_loop_new (_gcontext_device, true) );
}
bool Device::initialized()
{
return Device::manager().monitor_initialized_;
}
int Device::numDevices()
{
access_.lock();
int ret = handles_.size();
access_.unlock();
return ret;
}
bool Device::exists(const std::string &device)
{
access_.lock();
auto h = std::find_if(handles_.cbegin(), handles_.cend(), hasDeviceName(device));
bool ret = (h != handles_.cend());
access_.unlock();
return ret;
}
struct hasDevice: public std::unary_function<DeviceSource*, bool>
{
inline bool operator()(const DeviceSource* elem) const {
return (elem && elem->device() == _d);
}
explicit hasDevice(const std::string &d) : _d(d) { }
private:
std::string _d;
};
std::string Device::name(int index)
{
std::string ret = "";
access_.lock();
if (index > -1 && index < (int) handles_.size())
ret = handles_[index].name;
access_.unlock();
return ret;
}
std::string Device::description(int index)
{
std::string ret = "";
access_.lock();
if (index > -1 && index < (int) handles_.size())
ret = handles_[index].pipeline;
access_.unlock();
return ret;
}
DeviceConfigSet Device::config(int index)
{
DeviceConfigSet ret;
access_.lock();
if (index > -1 && index < (int) handles_.size())
ret = handles_[index].configs;
access_.unlock();
return ret;
}
int Device::index(const std::string &device)
{
int i = -1;
access_.lock();
auto h = std::find_if(handles_.cbegin(), handles_.cend(), hasDeviceName(device));
if (h != handles_.cend())
i = std::distance(handles_.cbegin(), h);
access_.unlock();
return i;
}
DeviceSource::DeviceSource(uint64_t id) : StreamSource(id), unplugged_(false)
{
// set symbol
symbol_ = new Symbol(Symbol::CAMERA, glm::vec3(0.75f, 0.75f, 0.01f));
symbol_->scale_.y = 1.5f;
}
DeviceSource::~DeviceSource()
{
unsetDevice();
}
void DeviceSource::unsetDevice()
{
// unregister this device source from a Device handler
auto h = std::find_if(Device::manager().handles_.begin(), Device::manager().handles_.end(), hasConnectedSource(this));
if (h != Device::manager().handles_.end())
{
// remove this pointer to the list of connected sources
h->connected_sources.remove(this);
// if this is the last source connected to the device handler
// the stream will be removed by the ~StreamSource destructor
// and the device handler should not keep reference to it
if (h->connected_sources.empty()) {
// if the cause of deletion is the unplugging of the device
if (unplugged_)
// remove the handle entirely
Device::manager().handles_.erase(h);
else
// otherwise just cancel the reference to the stream
h->stream = nullptr;
}
// else this means another DeviceSource is using this stream
// and we should avoid to delete the stream in the ~StreamSource destructor
else
stream_ = nullptr;
}
}
void DeviceSource::setDevice(const std::string &devicename)
{
if (device_.compare(devicename) == 0)
return;
// instanciate and wait for monitor initialization if not already initialized
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
Device::manager().monitor_initialization_.wait(lck, Device::initialized);
// if changing device
if (!device_.empty())
unsetDevice();
// remember device name
device_ = devicename;
// check existence of a device handle with that name
auto h = std::find_if(Device::manager().handles_.begin(), Device::manager().handles_.end(), hasDeviceName(device_));
if ( h != Device::manager().handles_.end()) {
// find if a DeviceHandle with this device name already has a stream
if ( h->stream != nullptr) {
// just use it !
stream_ = h->stream;
}
else {
// start filling in the gstreamer pipeline
std::ostringstream pipeline;
pipeline << h->pipeline;
// test the device and get config
DeviceConfigSet confs = h->configs;
#ifdef DEVICE_DEBUG
Log::Info("Device %s supported configs:", devicename.c_str());
for( DeviceConfigSet::iterator it = confs.begin(); it != confs.end(); ++it ){
float fps = static_cast<float>((*it).fps_numerator) / static_cast<float>((*it).fps_denominator);
Log::Info(" - %s %s %d x %d %.1f fps", (*it).stream.c_str(), (*it).format.c_str(), (*it).width, (*it).height, fps);
}
#endif
if (!confs.empty()) {
DeviceConfig best = *confs.rbegin();
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
Log::Info("Device %s selected its optimal config: %s %s %dx%d@%.1ffps", device_.c_str(), best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
pipeline << " ! " << best.stream;
if (!best.format.empty())
pipeline << ",format=" << best.format;
pipeline << ",framerate=" << best.fps_numerator << "/" << best.fps_denominator;
pipeline << ",width=" << best.width;
pipeline << ",height=" << best.height;
if ( best.stream.find("jpeg") != std::string::npos )
pipeline << " ! jpegdec";
if ( device_.find("Screen") != std::string::npos )
pipeline << " ! videoconvert ! video/x-raw,format=RGB ! queue max-size-buffers=3";
pipeline << " ! videoconvert";
// delete and reset render buffer to enforce re-init of StreamSource
if (renderbuffer_)
delete renderbuffer_;
renderbuffer_ = nullptr;
// new stream
if (stream_)
delete stream_;
stream_ = h->stream = new Stream;
// open gstreamer
h->stream->open( pipeline.str(), best.width, best.height);
h->stream->play(true);
}
}
// reference this source in the handle
h->connected_sources.push_back(this);
// will be ready after init and one frame rendered
ready_ = false;
}
else {
unplugged_ = true;
Log::Warning("No such device '%s'", device_.c_str());
}
}
void DeviceSource::setActive (bool on)
{
bool was_active = active_;
// try to activate (may fail if source is cloned)
Source::setActive(on);
if (stream_) {
// change status of stream (only if status changed)
if (active_ != was_active) {
// activate a source if any of the handled device source is active
auto h = std::find_if(Device::manager().handles_.begin(), Device::manager().handles_.end(), hasConnectedSource(this));
if (h != Device::manager().handles_.end()) {
bool streamactive = false;
for (auto sit = h->connected_sources.begin(); sit != h->connected_sources.end(); ++sit) {
if ( (*sit)->active_)
streamactive = true;
}
stream_->enable(streamactive);
}
}
}
}
void DeviceSource::accept(Visitor& v)
{
Source::accept(v);
if (!failed())
v.visit(*this);
}
bool DeviceSource::failed() const
{
return unplugged_ || StreamSource::failed();
}
DeviceConfigSet Device::getDeviceConfigs(const std::string &src_description)
{
DeviceConfigSet configs;
// create dummy pipeline to be tested
std::string description = src_description;
description += " name=devsrc ! fakesink name=sink";
// parse pipeline descriptor
GError *error = NULL;
GstElement *pipeline_ = gst_parse_launch (description.c_str(), &error);
if (error != NULL) {
Log::Warning("DeviceSource Could not construct test pipeline %s:\n%s", description.c_str(), error->message);
g_clear_error (&error);
return configs;
}
// get the pipeline element named "devsrc" from the Device class
GstElement *elem = gst_bin_get_by_name (GST_BIN (pipeline_), "devsrc");
if (elem) {
// initialize the pipeline
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PAUSED);
if (ret != GST_STATE_CHANGE_FAILURE) {
// get the first pad and its content
GstIterator *iter = gst_element_iterate_src_pads(elem);
GValue vPad = G_VALUE_INIT;
GstPad* pad_ret = NULL;
if (gst_iterator_next(iter, &vPad) == GST_ITERATOR_OK)
{
pad_ret = GST_PAD(g_value_get_object(&vPad));
GstCaps *device_caps = gst_pad_query_caps (pad_ret, NULL);
// loop over all caps offered by the pad
int C = gst_caps_get_size(device_caps);
for (int c = 0; c < C; ++c) {
// get GST cap
GstStructure *decice_cap_struct = gst_caps_get_structure (device_caps, c);
#ifdef GST_DEVICE_DEBUG
gchar *capstext = gst_structure_to_string (decice_cap_struct);
g_print("\nDevice caps: %s", capstext);
g_free(capstext);
#endif
// fill our config
DeviceConfig config;
// not managing opengl texture-target types
// TODO: support input devices texture-target video/x-raw(memory:GLMemory) for improved pipeline
if ( gst_structure_has_field (decice_cap_struct, "texture-target"))
continue;
// NAME : typically video/x-raw or image/jpeg
config.stream = gst_structure_get_name (decice_cap_struct);
// FORMAT : typically BGRA or YUVY
if ( gst_structure_has_field (decice_cap_struct, "format")) {
// get generic value
const GValue *val = gst_structure_get_value(decice_cap_struct, "format");
// if its a list of format string
if ( GST_VALUE_HOLDS_LIST(val)) {
int N = gst_value_list_get_size(val);
for (int n = 0; n < N; n++ ){
std::string f = gst_value_serialize( gst_value_list_get_value(val, n) );
// preference order : 1) RGBx, 2) JPEG, 3) ALL OTHER
// select f if it contains R (e.g. for RGBx) and not already RGB in config
if ( (f.find("R") != std::string::npos) && (config.format.find("R") == std::string::npos ) ) {
config.format = f;
break;
}
// default, take at least one if nothing yet in config
else if ( config.format.empty() )
config.format = f;
}
}
// single format
else {
config.format = gst_value_serialize(val);
}
}
// FRAMERATE : can be a fraction of a list of fractions
if ( gst_structure_has_field (decice_cap_struct, "framerate")) {
// get generic value
const GValue *val = gst_structure_get_value(decice_cap_struct, "framerate");
// if its a single fraction
if ( GST_VALUE_HOLDS_FRACTION(val)) {
config.fps_numerator = gst_value_get_fraction_numerator(val);
config.fps_denominator= gst_value_get_fraction_denominator(val);
}
// if its a range of fraction; take the max
else if ( GST_VALUE_HOLDS_FRACTION_RANGE(val)) {
config.fps_numerator = gst_value_get_fraction_numerator(gst_value_get_fraction_range_max(val));
config.fps_denominator= gst_value_get_fraction_denominator(gst_value_get_fraction_range_max(val));
}
// deal otherwise with a list of fractions; find the max
else if ( GST_VALUE_HOLDS_LIST(val)) {
gdouble fps_max = 1.0;
// loop over all fractions
int N = gst_value_list_get_size(val);
for (int i = 0; i < N; ++i ){
const GValue *frac = gst_value_list_get_value(val, i);
// read one fraction in the list
if ( GST_VALUE_HOLDS_FRACTION(frac)) {
int n = gst_value_get_fraction_numerator(frac);
int d = gst_value_get_fraction_denominator(frac);
// keep only the higher FPS
gdouble f = 1.0;
gst_util_fraction_to_double( n, d, &f );
if ( f > fps_max ) {
config.fps_numerator = n;
config.fps_denominator = d;
fps_max = f;
}
}
}
}
}
else {
// default
config.fps_numerator = 30;
config.fps_denominator = 1;
}
// WIDTH and HEIGHT
if ( gst_structure_has_field (decice_cap_struct, "width"))
gst_structure_get_int (decice_cap_struct, "width", &config.width);
if ( gst_structure_has_field (decice_cap_struct, "height"))
gst_structure_get_int (decice_cap_struct, "height", &config.height);
// add this config
configs.insert(config);
}
}
gst_iterator_free(iter);
// terminate pipeline
gst_element_set_state (pipeline_, GST_STATE_NULL);
}
g_object_unref (elem);
}
gst_object_unref (pipeline_);
return configs;
}
glm::ivec2 DeviceSource::icon() const
{
if ( device_.find("Screen") != std::string::npos )
return glm::ivec2(ICON_SOURCE_DEVICE_SCREEN);
else
return glm::ivec2(ICON_SOURCE_DEVICE);
}
std::string DeviceSource::info() const
{
if ( device_.find("Screen") != std::string::npos )
return "Screen capture";
else
return "Device";
}