New Audio recording

This commit is contained in:
Bruno Herbelin
2023-11-05 17:02:10 +01:00
parent 4eeb02d9d4
commit 03931cb232
12 changed files with 764 additions and 245 deletions

368
src/Audio.cpp Normal file
View File

@@ -0,0 +1,368 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2023 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 <thread>
#include <algorithm>
#include <gst/pbutils/gstdiscoverer.h>
#include <gst/pbutils/pbutils.h>
#include <gst/gst.h>
#include "Settings.h"
#include "Log.h"
#include "Audio.h"
#ifndef NDEBUG
//#define AUDIO_DEBUG
#endif
Audio::Audio(): monitor_(nullptr), monitor_initialized_(false)
{
std::thread(launchMonitoring, this).detach();
}
void Audio::launchMonitoring(Audio *d)
{
// gstreamer monitoring of devices
d->monitor_ = gst_device_monitor_new ();
gst_device_monitor_set_show_all_devices(d->monitor_, true);
// watching all video stream sources
GstCaps *caps = gst_caps_new_empty_simple ("audio/x-raw");
gst_device_monitor_add_filter (d->monitor_, "Audio/Source", caps);
gst_caps_unref (caps);
// 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);
// 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_audio_monitor, NULL);
gst_object_unref (bus);
// gst_device_monitor_set_show_all_devices(d->monitor_, false);
if ( !gst_device_monitor_start (d->monitor_) )
Log::Info("Audio discovery failed.");
// 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) );
}
void Audio::initialize()
{
// instanciate and wait for monitor initialization if not already initialized
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
Audio::manager().monitor_initialization_.wait(lck, Audio::_initialized);
}
bool Audio::_initialized()
{
return Audio::manager().monitor_initialized_;
}
gboolean
Audio::callback_audio_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;
}
int Audio::numDevices()
{
access_.lock();
int ret = handles_.size();
access_.unlock();
return ret;
}
struct hasAudioName
{
inline bool operator()(const AudioHandle &elem) const {
return (elem.name.compare(_name) == 0);
}
explicit hasAudioName(const std::string &name) : _name(name) { }
private:
std::string _name;
};
bool Audio::exists(const std::string &device)
{
access_.lock();
auto h = std::find_if(handles_.cbegin(), handles_.cend(), hasAudioName(device));
bool ret = (h != handles_.cend());
access_.unlock();
return ret;
}
int Audio::index(const std::string &device)
{
int i = -1;
access_.lock();
auto h = std::find_if(handles_.cbegin(), handles_.cend(), hasAudioName(device));
if (h != handles_.cend())
i = std::distance(handles_.cbegin(), h);
access_.unlock();
return i;
}
std::string Audio::name(int index)
{
std::string ret = "";
access_.lock();
if (index > -1 && index < (int) handles_.size())
ret = handles_[index].name;
access_.unlock();
return ret;
}
bool Audio::is_monitor(int index)
{
bool ret = false;
access_.lock();
if (index > -1 && index < (int) handles_.size())
ret = handles_[index].is_monitor;
access_.unlock();
return ret;
}
std::string Audio::pipeline(int index)
{
std::string ret = "";
access_.lock();
if (index > -1 && index < (int) handles_.size())
ret = handles_[index].pipeline;
access_.unlock();
return ret;
}
// copied from gst-inspect-1.0. perfect for identifying devices
// https://github.com/freedesktop/gstreamer-gst-plugins-base/blob/master/tools/gst-device-monitor.c
static gchar *get_launch_line(::GstDevice *device)
{
static const char *const ignored_propnames[] = { "name", "parent", "direction", "template", "caps", nullptr };
GString * launch_line;
GstElement * element;
GstElement * pureelement;
GParamSpec ** properties, *property;
GValue value = G_VALUE_INIT;
GValue pvalue = G_VALUE_INIT;
guint i, number_of_properties;
GstElementFactory * factory;
element = gst_device_create_element(device, nullptr);
if (!element)
return nullptr;
factory = gst_element_get_factory(element);
if (!factory) {
gst_object_unref(element);
return nullptr;
}
if (!gst_plugin_feature_get_name(factory)) {
gst_object_unref(element);
return nullptr;
}
launch_line = g_string_new(gst_plugin_feature_get_name(factory));
pureelement = gst_element_factory_create(factory, nullptr);
/* get paramspecs and show non-default properties */
properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(element), &number_of_properties);
if (properties) {
for (i = 0; i < number_of_properties; i++) {
gint j;
gboolean ignore = FALSE;
property = properties[i];
/* skip some properties */
if ((property->flags & G_PARAM_READWRITE) != G_PARAM_READWRITE)
continue;
for (j = 0; ignored_propnames[j]; j++)
if (!g_strcmp0(ignored_propnames[j], property->name))
ignore = TRUE;
if (ignore)
continue;
/* Can't use _param_value_defaults () because sub-classes modify the
* values already.
*/
g_value_init(&value, property->value_type);
g_value_init(&pvalue, property->value_type);
g_object_get_property(G_OBJECT(element), property->name, &value);
g_object_get_property(G_OBJECT(pureelement), property->name, &pvalue);
if (gst_value_compare(&value, &pvalue) != GST_VALUE_EQUAL) {
gchar *valuestr = gst_value_serialize(&value);
if (!valuestr) {
GST_WARNING("Could not serialize property %s:%s", GST_OBJECT_NAME(element), property->name);
g_free(valuestr);
goto next;
}
g_string_append_printf(launch_line, " %s=%s", property->name, valuestr);
g_free(valuestr);
}
next:
g_value_unset(&value);
g_value_unset(&pvalue);
}
g_free(properties);
}
gst_object_unref(element);
gst_object_unref(pureelement);
return g_string_free(launch_line, FALSE);
}
void Audio::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(), hasAudioName(device_name) );
if ( handle == handles_.cend() ) {
gchar *ll = get_launch_line(device);
// add if valid pipeline
if (ll)
{
AudioHandle dev;
dev.name = device_name;
dev.pipeline = std::string(ll);
dev.is_monitor = dev.pipeline.compare( dev.pipeline.size()-7, 7, "monitor") == 0;
#ifdef AUDIO_DEBUG
GstStructure *stru = gst_device_get_properties(device);
g_print("\nName: '%s'\nProperties: %s", device_name, gst_structure_to_string(stru) );
g_print("\nPipeline: %s\n", dev.pipeline.c_str());
#endif
handles_.push_back(dev);
Log::Info("Audio device '%s' is plugged-in.", device_name);
}
}
// unlock access
access_.unlock();
g_free (device_name);
}
void Audio::remove(GstDevice *device)
{
if (device==nullptr)
return;
std::string devicename = 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(), hasAudioName(devicename) );
if ( handle != handles_.cend() ) {
// inform the user
Log::Info("Audio device '%s' unplugged.", devicename.c_str());
// Warning if the audio device used for recording is unplugged
if (Settings::application.record.audio_device.compare(devicename) == 0) {
Log::Warning("Audio device for recording was unplugged.");
// reset settings
Settings::application.record.audio_device = "";
}
// remove the handle
handles_.erase(handle);
}
// unlock access
access_.unlock();
}

62
src/Audio.h Normal file
View File

@@ -0,0 +1,62 @@
#ifndef AUDIO_H
#define AUDIO_H
#include <string>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <gst/gst.h>
struct AudioHandle {
std::string name;
bool is_monitor;
std::string pipeline;
AudioHandle() : is_monitor(false) { }
};
class Audio
{
Audio();
Audio(Audio const& copy) = delete;
Audio& operator=(Audio const& copy) = delete;
public:
static Audio& manager()
{
// The only instance
static Audio _instance;
return _instance;
}
void initialize();
int numDevices ();
std::string name (int index);
std::string pipeline (int index);
bool is_monitor (int index);
int index (const std::string &device);
bool exists (const std::string &device);
static gboolean callback_audio_monitor (GstBus *, GstMessage *, gpointer);
private:
static void launchMonitoring(Audio *d);
static bool _initialized();
void add(GstDevice *device);
void remove(GstDevice *device);
GstDeviceMonitor *monitor_;
std::condition_variable monitor_initialization_;
bool monitor_initialized_;
bool monitor_unplug_event_;
std::mutex access_;
std::vector< AudioHandle > handles_;
};
#endif // AUDIO_H

View File

@@ -99,6 +99,7 @@ set(VMIX_SRCS
MousePointer.cpp MousePointer.cpp
Grid.cpp Grid.cpp
Playlist.cpp Playlist.cpp
Audio.cpp
) )
##### #####

View File

@@ -33,7 +33,7 @@
#include "DeviceSource.h" #include "DeviceSource.h"
#ifndef NDEBUG #ifndef NDEBUG
#define DEVICE_DEBUG //#define DEVICE_DEBUG
#endif #endif
@@ -173,11 +173,12 @@ void Device::add(GstDevice *device)
dev.pipeline = p; dev.pipeline = p;
dev.configs = confs; dev.configs = confs;
dev.properties = get_device_properties (device); dev.properties = get_device_properties (device);
#ifdef GST_DEVICE_DEBUG #ifdef DEVICE_DEBUG
g_print("\n%s %s", device_name, gst_structure_to_string(stru) ); GstStructure *stru = gst_device_get_properties(device);
g_print("\n%s: %s\n", device_name, gst_structure_to_string(stru) );
#endif #endif
handles_.push_back(dev); handles_.push_back(dev);
Log::Info("Device %s plugged.", device_name); Log::Info("Device '%s' is plugged-in.", device_name);
} }
} }
@@ -203,7 +204,7 @@ void Device::remove(GstDevice *device)
// just inform the user if there is no source connected // just inform the user if there is no source connected
if (handle->connected_sources.empty()) { if (handle->connected_sources.empty()) {
Log::Info("Device %s unplugged.", devicename.c_str()); Log::Info("Device '%s' unplugged.", devicename.c_str());
} }
else { else {
// otherwise unplug all sources and close their streams // otherwise unplug all sources and close their streams
@@ -221,7 +222,7 @@ void Device::remove(GstDevice *device)
// all connected sources are set to unplugged // all connected sources are set to unplugged
(*sit)->unplug(); (*sit)->unplug();
// and finally inform user // and finally inform user
Log::Warning("Device %s unplugged: sources %s disconnected.", Log::Warning("Device '%s' unplugged: sources %s disconnected.",
devicename.c_str(), (*sit)->name().c_str()); devicename.c_str(), (*sit)->name().c_str());
} }
} }
@@ -234,7 +235,7 @@ void Device::remove(GstDevice *device)
access_.unlock(); access_.unlock();
} }
Device::Device(): monitor_(nullptr), monitor_initialized_(false), monitor_unplug_event_(false) Device::Device(): monitor_(nullptr), monitor_initialized_(false)
{ {
std::thread(launchMonitoring, this).detach(); std::thread(launchMonitoring, this).detach();
} }

View File

@@ -27,7 +27,7 @@ public:
// specific interface // specific interface
void setDevice(const std::string &devicename); void setDevice(const std::string &devicename);
inline std::string device() const { return device_; } inline std::string device() const { return device_; }
void reconnect(); void reconnect();
glm::ivec2 icon() const override; glm::ivec2 icon() const override;
@@ -99,7 +99,6 @@ private:
GstDeviceMonitor *monitor_; GstDeviceMonitor *monitor_;
std::condition_variable monitor_initialization_; std::condition_variable monitor_initialization_;
bool monitor_initialized_; bool monitor_initialized_;
bool monitor_unplug_event_;
}; };

View File

@@ -320,7 +320,8 @@ void FrameGrabber::stop ()
active_ = false; active_ = false;
// send end of stream // send end of stream
gst_app_src_end_of_stream (src_); gst_element_send_event (pipeline_, gst_event_new_eos ());
} }
std::string FrameGrabber::info() const std::string FrameGrabber::info() const

View File

@@ -482,14 +482,14 @@ void MediaPlayer::execute_open()
Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(), Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(),
timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps()); timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps());
if (media_.hasaudio) if (media_.hasaudio) {
Log::Info("MediaPlayer %s Audio track %s", std::to_string(id_).c_str(), audio_enabled_ ? "enabled" : "disabled"); Log::Info("MediaPlayer %s Audio track %s", std::to_string(id_).c_str(), audio_enabled_ ? "enabled" : "disabled");
if (audio_enabled_)
setAudioVolume();
}
opened_ = true; opened_ = true;
// set volume
setAudioVolume();
// register media player // register media player
MediaPlayer::registered_.push_back(this); MediaPlayer::registered_.push_back(this);
} }
@@ -1716,7 +1716,9 @@ void MediaPlayer::setAudioVolume(int vol)
else if ( audio_volume_mix_ == MediaPlayer::VOLUME_MULT_1 ) else if ( audio_volume_mix_ == MediaPlayer::VOLUME_MULT_1 )
new_vol *= (gdouble) (audio_volume_[1]); new_vol *= (gdouble) (audio_volume_[1]);
gst_stream_volume_set_volume (GST_STREAM_VOLUME (pipeline_), GST_STREAM_VOLUME_FORMAT_LINEAR, new_vol);
g_object_set ( G_OBJECT (pipeline_), "volume", new_vol, NULL);
// gst_stream_volume_set_volume (GST_STREAM_VOLUME (pipeline_), GST_STREAM_VOLUME_FORMAT_LINEAR, new_vol);
} }
} }

View File

@@ -37,6 +37,7 @@
#include "GstToolkit.h" #include "GstToolkit.h"
#include "SystemToolkit.h" #include "SystemToolkit.h"
#include "Log.h" #include "Log.h"
#include "Audio.h"
#include "Recorder.h" #include "Recorder.h"
@@ -336,7 +337,7 @@ std::string VideoRecorder::init(GstCaps *caps)
timestamp_on_clock_ = Settings::application.record.priority_mode < 1; timestamp_on_clock_ = Settings::application.record.priority_mode < 1;
// create a gstreamer pipeline // create a gstreamer pipeline
std::string description = "appsrc name=src ! videoconvert ! "; std::string description = "appsrc name=src ! videoconvert ! queue ! ";
if (Settings::application.record.profile < 0 || Settings::application.record.profile >= DEFAULT) if (Settings::application.record.profile < 0 || Settings::application.record.profile >= DEFAULT)
Settings::application.record.profile = H264_STANDARD; Settings::application.record.profile = H264_STANDARD;
@@ -361,25 +362,46 @@ std::string VideoRecorder::init(GstCaps *caps)
else else
return std::string("Video Recording : Failed to create folder ") + folder; return std::string("Video Recording : Failed to create folder ") + folder;
} }
else if( Settings::application.record.profile == VP8) {
// if sequencial file naming
if (Settings::application.record.naming_mode == 0 )
filename_ = SystemToolkit::filename_sequential(Settings::application.record.path, basename_, "webm");
// or prefixed with date
else
filename_ = SystemToolkit::filename_dateprefix(Settings::application.record.path, basename_, "webm");
description += "webmmux ! filesink name=sink";
}
else { else {
// if sequencial file naming
if (Settings::application.record.naming_mode == 0 )
filename_ = SystemToolkit::filename_sequential(Settings::application.record.path, basename_, "mov");
// or prefixed with date
else
filename_ = SystemToolkit::filename_dateprefix(Settings::application.record.path, basename_, "mov");
description += "qtmux ! filesink name=sink"; // Add Audio to pipeline
if (!Settings::application.record.audio_device.empty()) {
// ensure the Audio manager has the device specified in settings
int current_audio = Audio::manager().index(Settings::application.record.audio_device);
if (current_audio > -1) {
description += "mux. ";
description += Audio::manager().pipeline(current_audio);
description += " ! audio/x-raw ! audioconvert ! audioresample ! ";
// select encoder depending on codec
if ( Settings::application.record.profile == VP8)
description += "opusenc ! opusparse ! queue ! ";
else
description += "voaacenc ! aacparse ! queue ! ";
Log::Info("Video Recording with audio (%s)", Audio::manager().pipeline(current_audio).c_str());
}
}
if ( Settings::application.record.profile == VP8) {
// if sequencial file naming
if (Settings::application.record.naming_mode == 0 )
filename_ = SystemToolkit::filename_sequential(Settings::application.record.path, basename_, "webm");
// or prefixed with date
else
filename_ = SystemToolkit::filename_dateprefix(Settings::application.record.path, basename_, "webm");
description += "webmmux name=mux ! filesink name=sink";
}
else {
// if sequencial file naming
if (Settings::application.record.naming_mode == 0 )
filename_ = SystemToolkit::filename_sequential(Settings::application.record.path, basename_, "mov");
// or prefixed with date
else
filename_ = SystemToolkit::filename_dateprefix(Settings::application.record.path, basename_, "mov");
description += "qtmux name=mux ! filesink name=sink";
}
} }
// parse pipeline descriptor // parse pipeline descriptor

View File

@@ -193,6 +193,7 @@ void Settings::Save(uint64_t runtime)
RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode); RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode);
RecordNode->SetAttribute("priority_mode", application.record.priority_mode); RecordNode->SetAttribute("priority_mode", application.record.priority_mode);
RecordNode->SetAttribute("naming_mode", application.record.naming_mode); RecordNode->SetAttribute("naming_mode", application.record.naming_mode);
RecordNode->SetAttribute("audio_device", application.record.audio_device.c_str());
pRoot->InsertEndChild(RecordNode); pRoot->InsertEndChild(RecordNode);
// Transition // Transition
@@ -500,6 +501,12 @@ void Settings::Load()
application.record.path = std::string(path_); application.record.path = std::string(path_);
else else
application.record.path = SystemToolkit::home_path(); application.record.path = SystemToolkit::home_path();
const char *dev_ = recordnode->Attribute("audio_device");
if (dev_)
application.record.audio_device = std::string(dev_);
else
application.record.audio_device = "";
} }
// Source // Source

View File

@@ -107,6 +107,7 @@ struct RecordConfig
int buffering_mode; int buffering_mode;
int priority_mode; int priority_mode;
int naming_mode; int naming_mode;
std::string audio_device;
RecordConfig() : path("") { RecordConfig() : path("") {
profile = 0; profile = 0;
@@ -117,6 +118,7 @@ struct RecordConfig
buffering_mode = 2; buffering_mode = 2;
priority_mode = 1; priority_mode = 1;
naming_mode = 1; naming_mode = 1;
audio_device = "";
} }
}; };

View File

@@ -78,6 +78,7 @@
#include "MultiFileRecorder.h" #include "MultiFileRecorder.h"
#include "MousePointer.h" #include "MousePointer.h"
#include "Playlist.h" #include "Playlist.h"
#include "Audio.h"
#include "UserInterfaceManager.h" #include "UserInterfaceManager.h"
@@ -5132,248 +5133,294 @@ void Navigator::RenderMainPannelPlaylist()
void Navigator::RenderMainPannelSettings() void Navigator::RenderMainPannelSettings()
{ {
// //
// Appearance // Appearance
// //
ImGui::Text("Settings"); ImGui::Text("Settings");
int v = Settings::application.accent_color; int v = Settings::application.accent_color;
ImGui::Spacing(); ImGui::Spacing();
ImGui::SetCursorPosX(0.5f * width_); ImGui::SetCursorPosX(0.5f * width_);
if (ImGui::RadioButton("##Color", &v, v)){ if (ImGui::RadioButton("##Color", &v, v)){
Settings::application.accent_color = (v+1)%3; Settings::application.accent_color = (v+1)%3;
ImGuiToolkit::SetAccentColor(static_cast<ImGuiToolkit::accent_color>(Settings::application.accent_color)); ImGuiToolkit::SetAccentColor(static_cast<ImGuiToolkit::accent_color>(Settings::application.accent_color));
// ask Views to update // ask Views to update
View::need_deep_update_++; View::need_deep_update_++;
} }
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGuiToolkit::ToolTip("Change accent color"); ImGuiToolkit::ToolTip("Change accent color");
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if ( ImGui::InputFloat("Scale", &Settings::application.scale, 0.1f, 0.1f, "%.1f")) { if ( ImGui::InputFloat("Scale", &Settings::application.scale, 0.1f, 0.1f, "%.1f")) {
Settings::application.scale = CLAMP(Settings::application.scale, 0.5f, 2.f); Settings::application.scale = CLAMP(Settings::application.scale, 0.5f, 2.f);
ImGui::GetIO().FontGlobalScale = Settings::application.scale; ImGui::GetIO().FontGlobalScale = Settings::application.scale;
} }
ImGuiToolkit::Indication("Scale the mouse pointer guiding grid to match aspect ratio.", ICON_FA_BORDER_NONE); ImGuiToolkit::Indication("Scale the mouse pointer guiding grid to match aspect ratio.", ICON_FA_BORDER_NONE);
ImGui::SameLine(); ImGui::SameLine();
ImGuiToolkit::ButtonSwitch( "Scaled grid", &Settings::application.proportional_grid); ImGuiToolkit::ButtonSwitch( "Scaled grid", &Settings::application.proportional_grid);
// //
// Recording preferences // Recording preferences
// //
ImGui::TextDisabled("Recording"); ImGui::TextDisabled("Recording");
// select CODEC and FPS // select CODEC and FPS
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("Codec", &Settings::application.record.profile, VideoRecorder::profile_name, IM_ARRAYSIZE(VideoRecorder::profile_name) ); ImGui::Combo("Codec", &Settings::application.record.profile, VideoRecorder::profile_name, IM_ARRAYSIZE(VideoRecorder::profile_name) );
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("Framerate", &Settings::application.record.framerate_mode, VideoRecorder::framerate_preset_name, IM_ARRAYSIZE(VideoRecorder::framerate_preset_name) ); ImGui::Combo("Framerate", &Settings::application.record.framerate_mode, VideoRecorder::framerate_preset_name, IM_ARRAYSIZE(VideoRecorder::framerate_preset_name) );
// compute number of frames in buffer and show warning sign if too low // compute number of frames in buffer and show warning sign if too low
const FrameBuffer *output = Mixer::manager().session()->frame(); const FrameBuffer *output = Mixer::manager().session()->frame();
if (output) { if (output) {
guint64 nb = 0; guint64 nb = 0;
nb = VideoRecorder::buffering_preset_value[Settings::application.record.buffering_mode] / (output->width() * output->height() * 4); nb = VideoRecorder::buffering_preset_value[Settings::application.record.buffering_mode] / (output->width() * output->height() * 4);
char buf[512]; snprintf(buf, 512, "Buffer at %s can contain %ld frames (%dx%d), i.e. %.1f sec", VideoRecorder::buffering_preset_name[Settings::application.record.buffering_mode], char buf[512]; snprintf(buf, 512, "Buffer at %s can contain %ld frames (%dx%d), i.e. %.1f sec", VideoRecorder::buffering_preset_name[Settings::application.record.buffering_mode],
(unsigned long)nb, output->width(), output->height(), (unsigned long)nb, output->width(), output->height(),
(float)nb / (float) VideoRecorder::framerate_preset_value[Settings::application.record.framerate_mode] ); (float)nb / (float) VideoRecorder::framerate_preset_value[Settings::application.record.framerate_mode] );
ImGuiToolkit::Indication(buf, 4, 6); ImGuiToolkit::Indication(buf, 4, 6);
ImGui::SameLine(0); ImGui::SameLine(0);
}
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderInt("Buffer", &Settings::application.record.buffering_mode, 0, IM_ARRAYSIZE(VideoRecorder::buffering_preset_name)-1,
VideoRecorder::buffering_preset_name[Settings::application.record.buffering_mode]);
ImGuiToolkit::Indication("Priority when buffer is full and recorder has to skip frames;\n"
ICON_FA_CARET_RIGHT " Duration:\n Variable framerate, correct duration.\n"
ICON_FA_CARET_RIGHT " Framerate:\n Correct framerate, shorter duration.",
ICON_FA_CHECK_DOUBLE);
ImGui::SameLine(0);
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("Priority", &Settings::application.record.priority_mode, "Duration\0Framerate\0");
//
// AUDIO
//
if (Settings::application.accept_audio) {
// Displayed name of current audio device
std::string current_audio = "None";
if (!Settings::application.record.audio_device.empty()) {
if (Audio::manager().exists(Settings::application.record.audio_device))
current_audio = Settings::application.record.audio_device;
else
Settings::application.record.audio_device = "";
} }
// help indication
ImGuiToolkit::Indication("Select the audio to merge into the recording;\n"
ICON_FA_MICROPHONE_ALT_SLASH " no audio\n "
ICON_FA_MICROPHONE_ALT " a microphone input\n "
ICON_FA_VOLUME_DOWN " an audio output",
ICON_FA_MUSIC);
ImGui::SameLine(0);
// Combo selector of audio device
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderInt("Buffer", &Settings::application.record.buffering_mode, 0, IM_ARRAYSIZE(VideoRecorder::buffering_preset_name)-1, if (ImGui::BeginCombo("Audio", current_audio.c_str())) {
VideoRecorder::buffering_preset_name[Settings::application.record.buffering_mode]); // No audio selection
if (ImGui::Selectable(ICON_FA_MICROPHONE_ALT_SLASH " None"))
Settings::application.record.audio_device = "";
// list of devices from Audio manager
for (int d = 0; d < Audio::manager().numDevices(); ++d) {
std::string namedev = Audio::manager().name(d);
std::string labeldev = (Audio::manager().is_monitor(d) ? ICON_FA_VOLUME_DOWN " "
: ICON_FA_MICROPHONE_ALT " ")
+ namedev;
if (ImGui::Selectable(labeldev.c_str())) {
Settings::application.record.audio_device = namedev;
}
}
ImGui::EndCombo();
}
if (!Settings::application.record.audio_device.empty() && ImGui::IsItemHovered())
ImGuiToolkit::ToolTip(current_audio.c_str());
ImGuiToolkit::Indication("Priority when buffer is full and recorder has to skip frames;\n" }
ICON_FA_CARET_RIGHT " Duration:\n Variable framerate, correct duration.\n"
ICON_FA_CARET_RIGHT " Framerate:\n Correct framerate, shorter duration.", //
ICON_FA_CHECK_DOUBLE); // Steaming preferences
//
ImGuiToolkit::Spacing();
ImGui::TextDisabled("Stream");
ImGuiToolkit::Indication("Peer-to-peer sharing local network\n\n"
"vimix can stream JPEG (default) or H264 (less bandwidth, higher encoding cost)", ICON_FA_SHARE_ALT_SQUARE);
ImGui::SameLine(0);
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("P2P codec", &Settings::application.stream_protocol, "JPEG\0H264\0");
if (VideoBroadcast::available()) {
char msg[256];
ImFormatString(msg, IM_ARRAYSIZE(msg), "SRT Broadcast\n\n"
"vimix listens to SRT requests on Port %d. "
"Example network addresses to call:\n"
" srt//%s:%d (localhost)\n"
" srt//%s:%d (local IP)",
Settings::application.broadcast_port,
NetworkToolkit::host_ips()[0].c_str(), Settings::application.broadcast_port,
NetworkToolkit::host_ips()[1].c_str(), Settings::application.broadcast_port );
ImGuiToolkit::Indication(msg, ICON_FA_GLOBE);
ImGui::SameLine(0); ImGui::SameLine(0);
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("Priority", &Settings::application.record.priority_mode, "Duration\0Framerate\0"); char bufport[7] = "";
snprintf(bufport, 7, "%d", Settings::application.broadcast_port);
// ImGui::InputTextWithHint("SRT Port", "7070", bufport, 6, ImGuiInputTextFlags_CharsDecimal);
// Steaming preferences if (ImGui::IsItemDeactivatedAfterEdit()){
// if ( BaseToolkit::is_a_number(bufport, &Settings::application.broadcast_port))
ImGuiToolkit::Spacing(); Settings::application.broadcast_port = CLAMP(Settings::application.broadcast_port, 1029, 49150);
ImGui::TextDisabled("Stream");
ImGuiToolkit::Indication("Peer-to-peer sharing local network\n\n"
"vimix can stream JPEG (default) or H264 (less bandwidth, higher encoding cost)", ICON_FA_SHARE_ALT_SQUARE);
ImGui::SameLine(0);
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("P2P codec", &Settings::application.stream_protocol, "JPEG\0H264\0");
if (VideoBroadcast::available()) {
char msg[256];
ImFormatString(msg, IM_ARRAYSIZE(msg), "SRT Broadcast\n\n"
"vimix listens to SRT requests on Port %d. "
"Example network addresses to call:\n"
" srt//%s:%d (localhost)\n"
" srt//%s:%d (local IP)",
Settings::application.broadcast_port,
NetworkToolkit::host_ips()[0].c_str(), Settings::application.broadcast_port,
NetworkToolkit::host_ips()[1].c_str(), Settings::application.broadcast_port );
ImGuiToolkit::Indication(msg, ICON_FA_GLOBE);
ImGui::SameLine(0);
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
char bufport[7] = "";
snprintf(bufport, 7, "%d", Settings::application.broadcast_port);
ImGui::InputTextWithHint("SRT Port", "7070", bufport, 6, ImGuiInputTextFlags_CharsDecimal);
if (ImGui::IsItemDeactivatedAfterEdit()){
if ( BaseToolkit::is_a_number(bufport, &Settings::application.broadcast_port))
Settings::application.broadcast_port = CLAMP(Settings::application.broadcast_port, 1029, 49150);
}
} }
}
if (ShmdataBroadcast::available()) { if (ShmdataBroadcast::available()) {
std::string _shm_socket_file = Settings::application.shm_socket_path; std::string _shm_socket_file = Settings::application.shm_socket_path;
if (_shm_socket_file.empty() || !SystemToolkit::file_exists(_shm_socket_file)) if (_shm_socket_file.empty() || !SystemToolkit::file_exists(_shm_socket_file))
_shm_socket_file = SystemToolkit::home_path(); _shm_socket_file = SystemToolkit::home_path();
_shm_socket_file = SystemToolkit::full_filename(_shm_socket_file, ".shm_vimix" + std::to_string(Settings::application.instance_id)); _shm_socket_file = SystemToolkit::full_filename(_shm_socket_file, ".shm_vimix" + std::to_string(Settings::application.instance_id));
char msg[256];
if (ShmdataBroadcast::available(ShmdataBroadcast::SHM_SHMDATASINK)) {
ImFormatString(msg, IM_ARRAYSIZE(msg), "Shared Memory\n\n"
"vimix can share to RAM with "
"gstreamer default 'shmsink' "
"and with 'shmdatasink'.\n"
"Socket file to connect to:\n%s\n",
_shm_socket_file.c_str());
}
else {
ImFormatString(msg, IM_ARRAYSIZE(msg), "Shared Memory\n\n"
"vimix can share to RAM with "
"gstreamer 'shmsink'.\n"
"Socket file to connect to:\n%s\n",
_shm_socket_file.c_str());
}
ImGuiToolkit::Indication(msg, ICON_FA_MEMORY);
ImGui::SameLine(0);
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
char bufsocket[64] = "";
snprintf(bufsocket, 64, "%s", Settings::application.shm_socket_path.c_str());
ImGui::InputTextWithHint("SHM path", SystemToolkit::home_path().c_str(), bufsocket, 64);
if (ImGui::IsItemDeactivatedAfterEdit()) {
Settings::application.shm_socket_path = bufsocket;
}
if (ShmdataBroadcast::available(ShmdataBroadcast::SHM_SHMDATASINK)) {
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::Combo("SHM plugin", &Settings::application.shm_method, "shmsink\0shmdatasink\0");
}
}
//
// OSC preferences
//
ImGuiToolkit::Spacing();
ImGui::TextDisabled("OSC");
char msg[256]; char msg[256];
ImFormatString(msg, IM_ARRAYSIZE(msg), "Open Sound Control\n\n" if (ShmdataBroadcast::available(ShmdataBroadcast::SHM_SHMDATASINK)) {
"vimix accepts OSC messages sent by UDP on Port %d and replies on Port %d." ImFormatString(msg, IM_ARRAYSIZE(msg), "Shared Memory\n\n"
"Example network addresses:\n" "vimix can share to RAM with "
" udp//%s:%d (localhost)\n" "gstreamer default 'shmsink' "
" udp//%s:%d (local IP)", "and with 'shmdatasink'.\n"
Settings::application.control.osc_port_receive, "Socket file to connect to:\n%s\n",
Settings::application.control.osc_port_send, _shm_socket_file.c_str());
NetworkToolkit::host_ips()[0].c_str(), Settings::application.control.osc_port_receive, }
NetworkToolkit::host_ips()[1].c_str(), Settings::application.control.osc_port_receive ); else {
ImGuiToolkit::Indication(msg, ICON_FA_NETWORK_WIRED); ImFormatString(msg, IM_ARRAYSIZE(msg), "Shared Memory\n\n"
"vimix can share to RAM with "
"gstreamer 'shmsink'.\n"
"Socket file to connect to:\n%s\n",
_shm_socket_file.c_str());
}
ImGuiToolkit::Indication(msg, ICON_FA_MEMORY);
ImGui::SameLine(0); ImGui::SameLine(0);
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
char bufreceive[7] = ""; char bufsocket[64] = "";
snprintf(bufreceive, 7, "%d", Settings::application.control.osc_port_receive); snprintf(bufsocket, 64, "%s", Settings::application.shm_socket_path.c_str());
ImGui::InputTextWithHint("Port in", "7000", bufreceive, 7, ImGuiInputTextFlags_CharsDecimal); ImGui::InputTextWithHint("SHM path", SystemToolkit::home_path().c_str(), bufsocket, 64);
if (ImGui::IsItemDeactivatedAfterEdit()){ if (ImGui::IsItemDeactivatedAfterEdit()) {
if ( BaseToolkit::is_a_number(bufreceive, &Settings::application.control.osc_port_receive)){ Settings::application.shm_socket_path = bufsocket;
Settings::application.control.osc_port_receive = CLAMP(Settings::application.control.osc_port_receive, 1029, 49150);
Control::manager().init();
}
} }
if (ShmdataBroadcast::available(ShmdataBroadcast::SHM_SHMDATASINK)) {
ImGui::SetCursorPosX(width_); ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
char bufsend[7] = ""; ImGui::Combo("SHM plugin", &Settings::application.shm_method, "shmsink\0shmdatasink\0");
snprintf(bufsend, 7, "%d", Settings::application.control.osc_port_send);
ImGui::InputTextWithHint("Port out", "7001", bufsend, 7, ImGuiInputTextFlags_CharsDecimal);
if (ImGui::IsItemDeactivatedAfterEdit()){
if ( BaseToolkit::is_a_number(bufsend, &Settings::application.control.osc_port_send)){
Settings::application.control.osc_port_send = CLAMP(Settings::application.control.osc_port_send, 1029, 49150);
Control::manager().init();
}
} }
}
ImGui::SetCursorPosX(width_); //
const float w = IMGUI_RIGHT_ALIGN - ImGui::GetFrameHeightWithSpacing(); // OSC preferences
ImGuiToolkit::ButtonOpenUrl( "Edit", Settings::application.control.osc_filename.c_str(), ImVec2(w, 0) ); //
ImGui::SameLine(0, 6); ImGuiToolkit::Spacing();
if ( ImGuiToolkit::IconButton(15, 12, "Reload") ) ImGui::TextDisabled("OSC");
char msg[256];
ImFormatString(msg, IM_ARRAYSIZE(msg), "Open Sound Control\n\n"
"vimix accepts OSC messages sent by UDP on Port %d and replies on Port %d."
"Example network addresses:\n"
" udp//%s:%d (localhost)\n"
" udp//%s:%d (local IP)",
Settings::application.control.osc_port_receive,
Settings::application.control.osc_port_send,
NetworkToolkit::host_ips()[0].c_str(), Settings::application.control.osc_port_receive,
NetworkToolkit::host_ips()[1].c_str(), Settings::application.control.osc_port_receive );
ImGuiToolkit::Indication(msg, ICON_FA_NETWORK_WIRED);
ImGui::SameLine(0);
ImGui::SetCursorPosX(width_);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
char bufreceive[7] = "";
snprintf(bufreceive, 7, "%d", Settings::application.control.osc_port_receive);
ImGui::InputTextWithHint("Port in", "7000", bufreceive, 7, ImGuiInputTextFlags_CharsDecimal);
if (ImGui::IsItemDeactivatedAfterEdit()){
if ( BaseToolkit::is_a_number(bufreceive, &Settings::application.control.osc_port_receive)){
Settings::application.control.osc_port_receive = CLAMP(Settings::application.control.osc_port_receive, 1029, 49150);
Control::manager().init(); Control::manager().init();
ImGui::SameLine(); }
ImGui::Text("Translator"); }
// ImGui::SetCursorPosX(width_);
// System preferences ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
// char bufsend[7] = "";
ImGuiToolkit::Spacing(); snprintf(bufsend, 7, "%d", Settings::application.control.osc_port_send);
ImGui::TextDisabled("System"); ImGui::InputTextWithHint("Port out", "7001", bufsend, 7, ImGuiInputTextFlags_CharsDecimal);
if (ImGui::IsItemDeactivatedAfterEdit()){
if ( BaseToolkit::is_a_number(bufsend, &Settings::application.control.osc_port_send)){
Settings::application.control.osc_port_send = CLAMP(Settings::application.control.osc_port_send, 1029, 49150);
Control::manager().init();
}
}
static bool need_restart = false; ImGui::SetCursorPosX(width_);
static bool vsync = (Settings::application.render.vsync > 0); const float w = IMGUI_RIGHT_ALIGN - ImGui::GetFrameHeightWithSpacing();
static bool multi = (Settings::application.render.multisampling > 0); ImGuiToolkit::ButtonOpenUrl( "Edit", Settings::application.control.osc_filename.c_str(), ImVec2(w, 0) );
static bool gpu = Settings::application.render.gpu_decoding; ImGui::SameLine(0, 6);
static bool audio = Settings::application.accept_audio; if ( ImGuiToolkit::IconButton(15, 12, "Reload") )
bool change = false; Control::manager().init();
// hardware support deserves more explanation ImGui::SameLine();
ImGuiToolkit::Indication("If enabled, tries to find a platform adapted hardware-accelerated " ImGui::Text("Translator");
"driver to decode (read) or encode (record) videos.", ICON_FA_MICROCHIP);
ImGui::SameLine(0);
if (Settings::application.render.gpu_decoding_available)
change |= ImGuiToolkit::ButtonSwitch( "Hardware en/decoding", &gpu);
else
ImGui::TextDisabled("Hardware en/decoding unavailable");
// audio support deserves more explanation //
ImGuiToolkit::Indication("If enabled, tries to find audio in openned videos " // System preferences
"and allows enabling audio for each source.", ICON_FA_VOLUME_OFF); //
ImGui::SameLine(0); ImGuiToolkit::Spacing();
change |= ImGuiToolkit::ButtonSwitch( "Audio support", &audio); ImGui::TextDisabled("System");
static bool need_restart = false;
static bool vsync = (Settings::application.render.vsync > 0);
static bool multi = (Settings::application.render.multisampling > 0);
static bool gpu = Settings::application.render.gpu_decoding;
static bool audio = Settings::application.accept_audio;
bool change = false;
// hardware support deserves more explanation
ImGuiToolkit::Indication("If enabled, tries to find a platform adapted hardware-accelerated "
"driver to decode (read) or encode (record) videos.", ICON_FA_MICROCHIP);
ImGui::SameLine(0);
if (Settings::application.render.gpu_decoding_available)
change |= ImGuiToolkit::ButtonSwitch( "Hardware en/decoding", &gpu);
else
ImGui::TextDisabled("Hardware en/decoding unavailable");
// audio support deserves more explanation
ImGuiToolkit::Indication("If enabled, tries to find audio in openned videos "
"and allows recording audio.", ICON_FA_VOLUME_OFF);
ImGui::SameLine(0);
change |= ImGuiToolkit::ButtonSwitch( "Audio (experimental)", &audio);
#ifndef NDEBUG #ifndef NDEBUG
change |= ImGuiToolkit::ButtonSwitch( "Vertical synchronization", &vsync); change |= ImGuiToolkit::ButtonSwitch( "Vertical synchronization", &vsync);
change |= ImGuiToolkit::ButtonSwitch( "Multisample antialiasing", &multi); change |= ImGuiToolkit::ButtonSwitch( "Multisample antialiasing", &multi);
#endif #endif
if (change) { if (change) {
need_restart = ( vsync != (Settings::application.render.vsync > 0) || need_restart = ( vsync != (Settings::application.render.vsync > 0) ||
multi != (Settings::application.render.multisampling > 0) || multi != (Settings::application.render.multisampling > 0) ||
gpu != Settings::application.render.gpu_decoding || gpu != Settings::application.render.gpu_decoding ||
audio != Settings::application.accept_audio ); audio != Settings::application.accept_audio );
} }
if (need_restart) { if (need_restart) {
ImGuiToolkit::Spacing(); ImGuiToolkit::Spacing();
if (ImGui::Button( ICON_FA_POWER_OFF " Quit & restart to apply", ImVec2(ImGui::GetContentRegionAvail().x - 50, 0))) { if (ImGui::Button( ICON_FA_POWER_OFF " Quit & restart to apply", ImVec2(ImGui::GetContentRegionAvail().x - 50, 0))) {
Settings::application.render.vsync = vsync ? 1 : 0; Settings::application.render.vsync = vsync ? 1 : 0;
Settings::application.render.multisampling = multi ? 3 : 0; Settings::application.render.multisampling = multi ? 3 : 0;
Settings::application.render.gpu_decoding = gpu; Settings::application.render.gpu_decoding = gpu;
Settings::application.accept_audio = audio; Settings::application.accept_audio = audio;
if (UserInterface::manager().TryClose()) if (UserInterface::manager().TryClose())
Rendering::manager().close(); Rendering::manager().close();
}
} }
}
} }

View File

@@ -30,6 +30,7 @@
#include "ControlManager.h" #include "ControlManager.h"
#include "Connection.h" #include "Connection.h"
#include "Metronome.h" #include "Metronome.h"
#include "Audio.h"
#if defined(APPLE) #if defined(APPLE)
extern "C"{ extern "C"{
@@ -158,6 +159,12 @@ int main(int argc, char *argv[])
gst_debug_set_active(FALSE); gst_debug_set_active(FALSE);
#endif #endif
///
/// AUDIO INIT
///
if ( Settings::application.accept_audio )
Audio::manager().initialize();
// callbacks to draw // callbacks to draw
Rendering::manager().pushBackDrawCallback(prepare); Rendering::manager().pushBackDrawCallback(prepare);
Rendering::manager().pushBackDrawCallback(drawScene); Rendering::manager().pushBackDrawCallback(drawScene);