diff --git a/src/Loopback.cpp b/src/Loopback.cpp index 588beba..f6379bb 100644 --- a/src/Loopback.cpp +++ b/src/Loopback.cpp @@ -17,25 +17,179 @@ * along with this program. If not, see . **/ -// Desktop OpenGL function loader -#include +#include +#include +#include +#include // gstreamer #include #include +#include +#include #include "defines.h" -#include "Settings.h" #include "GstToolkit.h" #include "SystemToolkit.h" -#include "FrameBuffer.h" #include "Log.h" #include "Loopback.h" -bool Loopback::system_loopback_initialized = false; +std::string Loopback::loopback_sink_; -#if defined(LINUX) +std::vector< std::pair > loopback_sink_alternatives_ { + {"v4l2sink", "videorate ! video/x-raw,format=RGB,framerate=30/1 ! v4l2sink"} + // TODO alternative sink for OSX? Windows? +}; + +bool Loopback::available() +{ + // test for availability once on first run + static bool _tested = false; + if (!_tested) { + + loopback_sink_.clear(); + + for (auto config = loopback_sink_alternatives_.cbegin(); + config != loopback_sink_alternatives_.cend() && loopback_sink_.empty(); ++config) { + if ( GstToolkit::has_feature(config->first) ) { + loopback_sink_ = config->second; + } + } + + _tested = true; + } + + return !loopback_sink_.empty(); +} + +Loopback::Loopback(int deviceid) : FrameGrabber(), loopback_device_(deviceid) +{ + frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, LOOPBACK_FPS); // fixed 30 FPS + + if ( !SystemToolkit::file_exists(device_name()) ) + throw std::runtime_error("Loopback system shall be initialized first."); +} + +std::string Loopback::device_name() const +{ + return std::string("/dev/video") + std::to_string(loopback_device_); +} + +std::string Loopback::init(GstCaps *caps) +{ + if (!Loopback::available()) + return std::string("Loopback : Not available"); + + // ignore + if (caps == nullptr) + return std::string("Loopback : Invalid caps"); + + // create a gstreamer pipeline + std::string description = "appsrc name=src ! videoconvert ! "; + + // complement pipeline with sink + description += Loopback::loopback_sink_ + " name=sink"; + + // parse pipeline descriptor + GError *error = NULL; + pipeline_ = gst_parse_launch (description.c_str(), &error); + if (error != NULL) { + std::string msg = std::string("Loopback : Could not construct pipeline ") + description + "\n" + std::string(error->message); + g_clear_error (&error); + return msg; + } + + // setup device sink + g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "sink")), + "device", device_name().c_str(), + "async", TRUE, + NULL); + + // setup custom app source + src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") ); + if (src_) { + + g_object_set (G_OBJECT (src_), + "is-live", TRUE, + "format", GST_FORMAT_TIME, + NULL); + + // configure stream + gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM); + gst_app_src_set_latency( src_, -1, 0); + + // Set buffer size + gst_app_src_set_max_bytes( src_, buffering_size_ ); + + // specify streaming framerate in the given caps + GstCaps *tmp = gst_caps_copy( caps ); + GValue v = { 0, }; + g_value_init (&v, GST_TYPE_FRACTION); + gst_value_set_fraction (&v, LOOPBACK_FPS, 1); // fixed FPS + gst_caps_set_value(tmp, "framerate", &v); + g_value_unset (&v); + + // instruct src to use the caps + caps_ = gst_caps_copy( tmp ); + gst_app_src_set_caps (src_, caps_); + gst_caps_unref (tmp); + + // setup callbacks + GstAppSrcCallbacks callbacks; + callbacks.need_data = FrameGrabber::callback_need_data; + callbacks.enough_data = FrameGrabber::callback_enough_data; + callbacks.seek_data = NULL; // stream type is not seekable + gst_app_src_set_callbacks( src_, &callbacks, this, NULL); + + } + else { + return std::string("Loopback : Could not configure source."); + } + + // start recording + GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING); + if (ret == GST_STATE_CHANGE_FAILURE) { + return std::string("Loopback : Could not open ") + device_name(); + } + + // all good + initialized_ = true; + + return std::string("Loopback started on ") + device_name(); +} + +void Loopback::terminate() +{ + // send EOS + gst_app_src_end_of_stream (src_); + + Log::Notify("Loopback to %s terminated.", device_name().c_str()); +} + +void Loopback::stop () +{ + // stop recording + FrameGrabber::stop (); + + // force finished + endofstream_ = true; + active_ = false; +} + +std::string Loopback::info() const +{ + std::ostringstream ret; + + if (!initialized_) + ret << "Loopback starting.."; + else if (active_) + ret << "V4L2 Loopback on " << device_name(); + else + ret << "Loopback terminated"; + + return ret.str(); +} /** * @@ -74,183 +228,3 @@ bool Loopback::system_loopback_initialized = false; * $ gst-launch-1.0 v4l2src device=/dev/video10 ! videoconvert ! autovideosink * $ gst-launch-1.0 videotestsrc ! v4l2sink device=/dev/video10 */ - -#include -#include -#include -#include - -std::string Loopback::system_loopback_name = "/dev/video10"; -std::string Loopback::system_loopback_pipeline = "appsrc name=src ! videoconvert ! videorate ! video/x-raw,framerate=30/1 ! v4l2sink sync=false name=sink"; - -bool Loopback::initializeSystemLoopback() -{ - if (!Loopback::systemLoopbackInitialized()) { - - // create script for asking sudo password - std::string sudoscript = SystemToolkit::full_filename(SystemToolkit::settings_path(), "sudo.sh"); - FILE *file = fopen(sudoscript.c_str(), "w"); - if (file) { - fprintf(file, "#!/bin/bash\n"); - fprintf(file, "zenity --password --title=Authentication\n"); - fclose(file); - - // make script executable - int fildes = 0; - fildes = open(sudoscript.c_str(), O_RDWR); - fchmod(fildes, S_IRWXU | S_IRWXG | S_IROTH | S_IWOTH); - close(fildes); - - // create command line for installing v4l2loopback - std::string cmdline = "export SUDO_ASKPASS=\"" + sudoscript + "\"\n"; - cmdline += "sudo -A apt install v4l2loopback-dkms 2>&1\n"; - cmdline += "sudo -A modprobe -r v4l2loopback 2>&1\n"; - cmdline += "sudo -A modprobe v4l2loopback exclusive_caps=1 video_nr=10 card_label=\"vimix loopback\" 2>&1\n"; - - // execute v4l2 command line - std::string report; - FILE *fp = popen(cmdline.c_str(), "r"); - if (fp != NULL) { - - // get stdout content from command line - char linestdout[PATH_MAX]; - while (fgets(linestdout, PATH_MAX, fp) != NULL) - report += linestdout; - - // error reported by pclose? - if (pclose(fp) != 0 ) - Log::Warning("Failed to initialize system v4l2loopback\n%s", report.c_str()); - // okay, probaly all good... - else - system_loopback_initialized = true; - } - else - Log::Warning("Failed to initialize system v4l2loopback\nCannot execute command line"); - - } - else - Log::Warning("Failed to initialize system v4l2loopback\nCannot create script", sudoscript.c_str()); - } - - return system_loopback_initialized; -} - -bool Loopback::systemLoopbackInitialized() -{ - // test if already initialized - if (!system_loopback_initialized) { - // check the existence of loopback device - if ( SystemToolkit::file_exists(system_loopback_name) ) - system_loopback_initialized = true; - } - - return system_loopback_initialized; -} - -#else - -std::string Loopback::system_loopback_name = "undefined"; -std::string Loopback::system_loopback_pipeline = ""; - - -bool Loopback::initializeSystemLoopback() -{ - system_loopback_initialized = false; - return false; -} - -bool Loopback::systemLoopbackInitialized() -{ - return false; -} -#endif - - -Loopback::Loopback() : FrameGrabber() -{ - frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 30); // fixed 30 FPS -} - -std::string Loopback::init(GstCaps *caps) -{ - // ignore - if (caps == nullptr) - return std::string("Invalid caps"); - - if (!Loopback::systemLoopbackInitialized()){ - return std::string("Loopback system shall be initialized first."); - } - - // create a gstreamer pipeline - std::string description = Loopback::system_loopback_pipeline; - - // parse pipeline descriptor - GError *error = NULL; - pipeline_ = gst_parse_launch (description.c_str(), &error); - if (error != NULL) { - std::string msg = std::string("Loopback : Could not construct pipeline ") + description + "\n" + std::string(error->message); - g_clear_error (&error); - return msg; - } - - // setup device sink - g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "sink")), - "device", Loopback::system_loopback_name.c_str(), - NULL); - - // setup custom app source - src_ = GST_APP_SRC( gst_bin_get_by_name (GST_BIN (pipeline_), "src") ); - if (src_) { - - g_object_set (G_OBJECT (src_), - "is-live", TRUE, - NULL); - - // configure stream - gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM); - gst_app_src_set_latency( src_, -1, 0); - - // Set buffer size - gst_app_src_set_max_bytes( src_, buffering_size_ ); - - // specify streaming framerate in the given caps - GstCaps *tmp = gst_caps_copy( caps ); - GValue v = { 0, }; - g_value_init (&v, GST_TYPE_FRACTION); - gst_value_set_fraction (&v, 30, 1); // fixed 30 FPS - gst_caps_set_value(tmp, "framerate", &v); - g_value_unset (&v); - - // instruct src to use the caps - caps_ = gst_caps_copy( tmp ); - gst_app_src_set_caps (src_, caps_); - gst_caps_unref (tmp); - - // setup callbacks - GstAppSrcCallbacks callbacks; - callbacks.need_data = FrameGrabber::callback_need_data; - callbacks.enough_data = FrameGrabber::callback_enough_data; - callbacks.seek_data = NULL; // stream type is not seekable - gst_app_src_set_callbacks( src_, &callbacks, this, NULL); - - } - else { - return std::string("Loopback : Could not configure source."); - } - - // start recording - GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING); - if (ret == GST_STATE_CHANGE_FAILURE) { - return std::string("Loopback : Could not open ") + Loopback::system_loopback_name; - } - - // all good - initialized_ = true; - - return std::string("Loopback started on ") + Loopback::system_loopback_name; -} - -void Loopback::terminate() -{ - Log::Notify("Loopback to %s terminated.", Loopback::system_loopback_name.c_str()); -} diff --git a/src/Loopback.h b/src/Loopback.h index 7f047c3..e92f784 100644 --- a/src/Loopback.h +++ b/src/Loopback.h @@ -1,30 +1,33 @@ #ifndef LOOPBACK_H #define LOOPBACK_H -#include - -#include -#include - #include "FrameGrabber.h" +#define LOOPBACK_DEFAULT_DEVICE 10 +#define LOOPBACK_FPS 30 class Loopback : public FrameGrabber { - static std::string system_loopback_pipeline; - static std::string system_loopback_name; - static bool system_loopback_initialized; +public: + Loopback(int deviceid = LOOPBACK_DEFAULT_DEVICE); + virtual ~Loopback() {} + static bool available(); + + inline int device_id() const { return loopback_device_; } + std::string device_name() const; + + void stop() override; + std::string info() const override; + +private: std::string init(GstCaps *caps) override; void terminate() override; -public: - - Loopback(); - - static bool systemLoopbackInitialized(); - static bool initializeSystemLoopback(); + // device information + int loopback_device_; + static std::string loopback_sink_; }; diff --git a/src/Settings.cpp b/src/Settings.cpp index a319502..6fdfd3d 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -127,6 +127,8 @@ void Settings::Save(uint64_t runtime) applicationNode->SetAttribute("pannel_history_mode", application.pannel_current_session_mode); applicationNode->SetAttribute("stream_protocol", application.stream_protocol); applicationNode->SetAttribute("broadcast_port", application.broadcast_port); + applicationNode->SetAttribute("loopback_camera", application.loopback_camera); + applicationNode->SetAttribute("shm_socket_path", application.shm_socket_path.c_str()); pRoot->InsertEndChild(applicationNode); // Widgets @@ -398,6 +400,12 @@ void Settings::Load() applicationNode->QueryIntAttribute("pannel_history_mode", &application.pannel_current_session_mode); applicationNode->QueryIntAttribute("stream_protocol", &application.stream_protocol); applicationNode->QueryIntAttribute("broadcast_port", &application.broadcast_port); + applicationNode->QueryIntAttribute("loopback_camera", &application.loopback_camera); + + // text attributes + const char *tmpstr = applicationNode->Attribute("shm_socket_path"); + if (tmpstr) + application.shm_socket_path = std::string(tmpstr); } // Widgets diff --git a/src/Settings.h b/src/Settings.h index 0ce3e5f..4fa6ea0 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -263,6 +263,8 @@ struct Application int stream_protocol; int broadcast_port; KnownHosts recentSRT; + int loopback_camera; + std::string shm_socket_path; // Settings of widgets WidgetsConfig widget; @@ -317,9 +319,11 @@ struct Application show_tooptips = true; accept_connections = false; stream_protocol = 0; - broadcast_port = 7070; + broadcast_port = 0; recentSRT.protocol = "srt://"; recentSRT.default_host = { "127.0.0.1", "7070"}; + loopback_camera = 0; + shm_socket_path = ""; pannel_current_session_mode = 0; current_view = 1; current_workspace= 1; diff --git a/src/ShmdataBroadcast.cpp b/src/ShmdataBroadcast.cpp index 8721dde..ca6efd9 100644 --- a/src/ShmdataBroadcast.cpp +++ b/src/ShmdataBroadcast.cpp @@ -27,7 +27,6 @@ #include #include "Log.h" -#include "Settings.h" #include "GstToolkit.h" #include "ShmdataBroadcast.h" @@ -49,11 +48,12 @@ bool ShmdataBroadcast::available() return _available; } -ShmdataBroadcast::ShmdataBroadcast(const std::string &socketpath): FrameGrabber(), socket_path_(socketpath), stopped_(false) +ShmdataBroadcast::ShmdataBroadcast(const std::string &socketpath): FrameGrabber(), socket_path_(socketpath) { - frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, DEFAULT_GRABBER_FPS); // fixed 30 FPS + frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, SHMDATA_FPS); // fixed 30 FPS - socket_path_ += std::to_string(Settings::application.instance_id); + if (socket_path_.empty()) + socket_path_ = SHMDATA_DEFAULT_PATH; } std::string ShmdataBroadcast::init(GstCaps *caps) @@ -105,7 +105,7 @@ std::string ShmdataBroadcast::init(GstCaps *caps) GstCaps *tmp = gst_caps_copy( caps ); GValue v = { 0, }; g_value_init (&v, GST_TYPE_FRACTION); - gst_value_set_fraction (&v, DEFAULT_GRABBER_FPS, 1); // fixed 30 FPS + gst_value_set_fraction (&v, SHMDATA_FPS, 1); // fixed 30 FPS gst_caps_set_value(tmp, "framerate", &v); g_value_unset (&v); @@ -141,22 +141,14 @@ std::string ShmdataBroadcast::init(GstCaps *caps) void ShmdataBroadcast::terminate() { - // send EOS - gst_app_src_end_of_stream (src_); + // force finished + endofstream_ = true; + active_ = false; Log::Notify("Shared Memory terminated after %s s.", GstToolkit::time_to_string(duration_).c_str()); } -void ShmdataBroadcast::stop () -{ - // stop recording - FrameGrabber::stop (); - - // force finished - endofstream_ = true; - active_ = false; -} std::string ShmdataBroadcast::info() const { diff --git a/src/ShmdataBroadcast.h b/src/ShmdataBroadcast.h index 2c7fd04..6bfb151 100644 --- a/src/ShmdataBroadcast.h +++ b/src/ShmdataBroadcast.h @@ -3,16 +3,19 @@ #include "FrameGrabber.h" +#define SHMDATA_DEFAULT_PATH "/tmp/shmdata_vimix" +#define SHMDATA_FPS 30 + class ShmdataBroadcast : public FrameGrabber { public: - ShmdataBroadcast(const std::string &socketpath = "/tmp/shmdata_vimix"); + ShmdataBroadcast(const std::string &socketpath = SHMDATA_DEFAULT_PATH); virtual ~ShmdataBroadcast() {} static bool available(); + inline std::string socket_path() const { return socket_path_; } - void stop() override; std::string info() const override; private: @@ -21,7 +24,6 @@ private: // connection information std::string socket_path_; - std::atomic stopped_; static std::string shmdata_sink_; }; diff --git a/src/Source.h b/src/Source.h index df3f579..9c6d022 100644 --- a/src/Source.h +++ b/src/Source.h @@ -23,7 +23,7 @@ #define ICON_SOURCE_RENDER 0, 2 #define ICON_SOURCE_CLONE 9, 2 #define ICON_SOURCE_GSTREAMER 16, 16 -#define ICON_SOURCE_SRT 11, 8 +#define ICON_SOURCE_SRT 15, 5 #define ICON_SOURCE 13, 11 class Visitor; diff --git a/src/UserInterfaceManager.cpp b/src/UserInterfaceManager.cpp index 82e8cc7..5fe7599 100644 --- a/src/UserInterfaceManager.cpp +++ b/src/UserInterfaceManager.cpp @@ -940,7 +940,8 @@ void UserInterface::showMenuEdit() navigator.hidePannel(); } if (ImGui::MenuItem( MENU_PASTE, SHORTCUT_PASTE, false, has_clipboard)) { - Mixer::manager().paste(clipboard); + if (clipboard) + Mixer::manager().paste(clipboard); navigator.hidePannel(); } if (ImGui::MenuItem( MENU_SELECTALL, SHORTCUT_SELECTALL)) { @@ -3811,11 +3812,8 @@ void SourceController::DrawButtonBar(ImVec2 bottom, float width) /// OutputPreview::OutputPreview() : WorkspaceWindow("OutputPreview"), - video_recorder_(nullptr), video_broadcaster_(nullptr) + video_recorder_(nullptr), video_broadcaster_(nullptr), loopback_broadcaster_(nullptr) { -#if defined(LINUX) - webcam_emulator_ = nullptr; -#endif recordFolderDialog = new DialogToolkit::OpenFolderDialog("Recording Location"); } @@ -3864,14 +3862,11 @@ void OutputPreview::Update() video_recorder_->stop(); } - // verify the broadcasters are valid (change to nullptr if invalid) + // verify the frame grabbers are valid (change to nullptr if invalid) FrameGrabbing::manager().verify( (FrameGrabber**) &video_broadcaster_); FrameGrabbing::manager().verify( (FrameGrabber**) &shm_broadcaster_); + FrameGrabbing::manager().verify( (FrameGrabber**) &loopback_broadcaster_); -#if defined(LINUX) - // verify the frame grabber for webcam emulator is valid - FrameGrabbing::manager().verify( (FrameGrabber**) &webcam_emulator_); -#endif } VideoRecorder *delayTrigger(VideoRecorder *g, std::chrono::milliseconds delay) { @@ -3902,13 +3897,15 @@ void OutputPreview::ToggleRecord(bool save_and_continue) } } -void OutputPreview::ToggleBroadcast() +void OutputPreview::ToggleVideoBroadcast() { if (video_broadcaster_) { video_broadcaster_->stop(); } else { - video_broadcaster_ = new VideoBroadcast; + if (Settings::application.broadcast_port<1000) + Settings::application.broadcast_port = BROADCAST_DEFAULT_PORT; + video_broadcaster_ = new VideoBroadcast(Settings::application.broadcast_port); FrameGrabbing::manager().add(video_broadcaster_); } } @@ -3919,17 +3916,40 @@ void OutputPreview::ToggleSharedMemory() shm_broadcaster_->stop(); } else { - shm_broadcaster_ = new ShmdataBroadcast; + if (Settings::application.shm_socket_path.empty()) + Settings::application.shm_socket_path = SHMDATA_DEFAULT_PATH + std::to_string(Settings::application.instance_id); + shm_broadcaster_ = new ShmdataBroadcast(Settings::application.shm_socket_path); FrameGrabbing::manager().add(shm_broadcaster_); } } +bool OutputPreview::ToggleLoopbackCamera() +{ + bool need_initialization = false; + if (loopback_broadcaster_) { + loopback_broadcaster_->stop(); + } + else { + if (Settings::application.loopback_camera < 1) + Settings::application.loopback_camera = LOOPBACK_DEFAULT_DEVICE; + Settings::application.loopback_camera += Settings::application.instance_id; + + try { + loopback_broadcaster_ = new Loopback(Settings::application.loopback_camera); + FrameGrabbing::manager().add(loopback_broadcaster_); + } + catch (const std::runtime_error &e) { + need_initialization = true; + Log::Info("%s", e.what()); + } + } + return need_initialization; +} + + void OutputPreview::Render() { - -#if defined(LINUX) bool openInitializeSystemLoopback = false; -#endif FrameBuffer *output = Mixer::manager().session()->frame(); if (output) @@ -4090,25 +4110,6 @@ void OutputPreview::Render() } if (ImGui::BeginMenu(ICON_FA_WIFI " Stream")) { - -#if defined(LINUX_NOT_YET_WORKING) - bool on = webcam_emulator_ != nullptr; - if ( ImGui::MenuItem( ICON_FA_CAMERA " Emulate video camera", NULL, &on) ) { - if (on) { - if (Loopback::systemLoopbackInitialized()) { - webcam_emulator_ = new Loopback; - FrameGrabbing::manager().add(webcam_emulator_); - } - else - openInitializeSystemLoopback = true; - } - else if (webcam_emulator_ != nullptr) { - webcam_emulator_->stop(); - webcam_emulator_ = nullptr; - } - } -#endif - // Stream sharing menu ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_STREAM, 0.9f)); if ( ImGui::MenuItem( ICON_FA_SHARE_ALT_SQUARE " Peer-to-peer sharing", NULL, &Settings::application.accept_connections) ) { @@ -4119,36 +4120,39 @@ void OutputPreview::Render() // list active streams: std::vector ls = Streaming::manager().listStreams(); - // Broadcasting menu ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.9f)); if (VideoBroadcast::available()) { - bool enabled = (video_broadcaster_!=nullptr); - if ( ImGui::MenuItem( ICON_FA_PODCAST " SRT Broadcast", NULL, &enabled) ) - ToggleBroadcast(); + if ( ImGui::MenuItem( ICON_FA_GLOBE " SRT Broadcast", NULL, videoBroadcastEnabled()) ) + ToggleVideoBroadcast(); } // Shared Memory menu if (ShmdataBroadcast::available()) { - bool enabled = (shm_broadcaster_!=nullptr); - if ( ImGui::MenuItem( ICON_FA_PROJECT_DIAGRAM " Shared Memory", NULL, &enabled) ) + if ( ImGui::MenuItem( ICON_FA_SHARE_ALT " Shared Memory", NULL, sharedMemoryEnabled()) ) ToggleSharedMemory(); } + + // Loopback camera menu + if (Loopback::available()) { + if ( ImGui::MenuItem( ICON_FA_VIDEO " Loopback Camera", NULL, loopbackCameraEnabled()) ) + openInitializeSystemLoopback = ToggleLoopbackCamera(); + } + ImGui::PopStyleColor(1); // Display list of active stream - if (ls.size()>0 || video_broadcaster_ || shm_broadcaster_) { + if (ls.size()>0 || videoBroadcastEnabled() || sharedMemoryEnabled() || loopbackCameraEnabled()) { ImGui::Separator(); - ImGui::MenuItem("Active streaming", nullptr, false, false); + ImGui::MenuItem("Active streams", nullptr, false, false); // First the list of peer 2 peer for (auto it = ls.begin(); it != ls.end(); ++it) ImGui::Text(" %s ", (*it).c_str() ); - // Second the SRT - if (video_broadcaster_) { + // SRT broadcast description + if (videoBroadcastEnabled()) { ImGui::Text(" %s ", video_broadcaster_->info().c_str()); - // copy text icon to give user the srt link to connect to ImVec2 draw_pos = ImGui::GetCursorPos(); ImGui::SetCursorPos(draw_pos + ImVec2(ImGui::GetContentRegionAvailWidth() - 1.2 * ImGui::GetTextLineHeightWithSpacing(), -0.8 * ImGui::GetFrameHeight()) ); @@ -4159,17 +4163,27 @@ void OutputPreview::Render() ImGui::SetCursorPos(draw_pos); } - // Third the SHMdata - if (shm_broadcaster_) { + // Shared memory broadcast description + if (sharedMemoryEnabled()) { ImGui::Text(" %s ", shm_broadcaster_->info().c_str()); - - // copy text icon to give user the socket path to connec to + // copy text icon to give user the socket path to connect to ImVec2 draw_pos = ImGui::GetCursorPos(); ImGui::SetCursorPos(draw_pos + ImVec2(ImGui::GetContentRegionAvailWidth() - 1.2 * ImGui::GetTextLineHeightWithSpacing(), -0.8 * ImGui::GetFrameHeight()) ); if (ImGuiToolkit::IconButton( ICON_FA_COPY, shm_broadcaster_->socket_path().c_str())) ImGui::SetClipboardText(shm_broadcaster_->socket_path().c_str()); ImGui::SetCursorPos(draw_pos); } + + // Loopback camera description + if (loopbackCameraEnabled()) { + ImGui::Text(" %s ", loopback_broadcaster_->info().c_str()); + // copy text icon to give user the device path to connect to + ImVec2 draw_pos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(draw_pos + ImVec2(ImGui::GetContentRegionAvailWidth() - 1.2 * ImGui::GetTextLineHeightWithSpacing(), -0.8 * ImGui::GetFrameHeight()) ); + if (ImGuiToolkit::IconButton( ICON_FA_COPY, loopback_broadcaster_->device_name().c_str())) + ImGui::SetClipboardText(loopback_broadcaster_->device_name().c_str()); + ImGui::SetCursorPos(draw_pos); + } } ImGui::EndMenu(); @@ -4201,88 +4215,83 @@ void OutputPreview::Render() ImGui::SetCursorScreenPos(draw_pos + ImVec2(imagesize.x - r, 6)); ImGui::Text(ICON_FA_INFO_CIRCLE); bool drawoverlay = ImGui::IsItemHovered(); + + // icon indicators + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); // recording indicator if (video_recorder_) { ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + r, draw_pos.y + r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_RECORD, 0.8f)); ImGui::Text(ICON_FA_CIRCLE " %s", video_recorder_->info().c_str() ); ImGui::PopStyleColor(1); - ImGui::PopFont(); } else if (!_video_recorders.empty()) { ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + r, draw_pos.y + r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); static double anim = 0.f; double a = sin(anim+=0.104); // 2 * pi / 60fps ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_RECORD, a)); ImGui::Text(ICON_FA_CIRCLE); ImGui::PopStyleColor(1); - ImGui::PopFont(); } // broadcast indicator + float vertical = r; if (video_broadcaster_) { - ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 2.5f * r, draw_pos.y + r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); + ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 2.5f * r, draw_pos.y + vertical)); if (video_broadcaster_->busy()) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.8f)); else ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.4f)); - ImGui::Text(ICON_FA_PODCAST); + ImGui::Text(ICON_FA_GLOBE); ImGui::PopStyleColor(1); - ImGui::PopFont(); + vertical += 2.f * r; } // shmdata indicator if (shm_broadcaster_) { - ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 4.5f * r, draw_pos.y + r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); + ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 2.5f * r, draw_pos.y + vertical)); if (shm_broadcaster_->busy()) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.8f)); else ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.4f)); - ImGui::Text(ICON_FA_PROJECT_DIAGRAM); + ImGui::Text(ICON_FA_SHARE_ALT); + ImGui::PopStyleColor(1); + vertical += 2.f * r; + } + // loopback camera indicator + if (loopback_broadcaster_) + { + ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 2.5f * r, draw_pos.y + vertical)); + if (loopback_broadcaster_->busy()) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.8f)); + else + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_BROADCAST, 0.4f)); + ImGui::Text(ICON_FA_VIDEO); ImGui::PopStyleColor(1); - ImGui::PopFont(); } // streaming indicator if (Settings::application.accept_connections) { ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 2.4f * r, draw_pos.y + imagesize.y - 2.f*r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); if ( Streaming::manager().busy()) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_STREAM, 0.8f)); else ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_STREAM, 0.4f)); ImGui::Text(ICON_FA_SHARE_ALT_SQUARE); ImGui::PopStyleColor(1); - ImGui::PopFont(); } // OUTPUT DISABLED indicator if (Settings::application.render.disabled) { ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + r, draw_pos.y + imagesize.y - 2.f*r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_RECORD, 0.8f)); ImGui::Text(ICON_FA_EYE_SLASH); ImGui::PopStyleColor(1); - ImGui::PopFont(); } -#if defined(LINUX) - // streaming indicator - if (webcam_emulator_) - { - ImGui::SetCursorScreenPos(ImVec2(draw_pos.x + imagesize.x - 2.5f * r, draw_pos.y + 2.f * r)); - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 0.8f)); - ImGui::Text(ICON_FA_CAMERA); - ImGui::PopStyleColor(1); - ImGui::PopFont(); - } -#endif + ImGui::PopFont(); + /// /// Info overlay over image @@ -4292,64 +4301,66 @@ void OutputPreview::Render() ImDrawList* draw_list = ImGui::GetWindowDrawList(); float h = 1.f; h += (Settings::application.accept_connections ? 1.f : 0.f); - h += (video_broadcaster_ ? 1.f : 0.f); - h += (shm_broadcaster_ ? 1.f : 0.f); draw_list->AddRectFilled(draw_pos, ImVec2(draw_pos.x + imagesize.x, draw_pos.y + h * r), IMGUI_COLOR_OVERLAY); ImGui::SetCursorScreenPos(draw_pos); ImGui::Text(" " ICON_FA_TV " %d x %d px, %.d fps", output->width(), output->height(), int(Mixer::manager().fps()) ); if (Settings::application.accept_connections) - ImGui::Text( " " ICON_FA_SHARE_ALT_SQUARE " %s (%ld peer)", + ImGui::Text( " " ICON_FA_SHARE_ALT_SQUARE " Available as %s (%ld peer connected)", Connection::manager().info().name.c_str(), Streaming::manager().listStreams().size() ); - if (video_broadcaster_) - ImGui::Text( " " ICON_FA_PODCAST " %s", video_broadcaster_->info().c_str() ); - if (shm_broadcaster_) - ImGui::Text( " " ICON_FA_PROJECT_DIAGRAM " %s", shm_broadcaster_->info().c_str() ); } ImGui::End(); } -#if defined(LINUX) + // Dialog to explain to the user how to initialize the loopback on the system if (openInitializeSystemLoopback && !ImGui::IsPopupOpen("Initialize System Loopback")) ImGui::OpenPopup("Initialize System Loopback"); if (ImGui::BeginPopupModal("Initialize System Loopback", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { +#if defined(LINUX) int w = 600; ImGui::Text("In order to enable the video4linux camera loopback,\n" - "'v4l2loopack' has to be installed and initialized on your machine\n\n" - "To do so, the following commands should be executed (admin rights):\n"); + "'v4l2loopack' has to be installed and initialized on your machine"); + ImGui::Spacing(); + ImGuiToolkit::ButtonOpenUrl("More information online on v4l2loopback", "https://github.com/umlaeute/v4l2loopback"); + ImGui::Spacing(); + ImGui::Text("To do so, the following commands should be executed\n(with admin rights):"); static char dummy_str[512]; sprintf(dummy_str, "sudo apt install v4l2loopback-dkms"); - ImGui::Text("Install v4l2loopack (once):"); + ImGui::NewLine(); + ImGui::Text("Install v4l2loopack (only once, and reboot):"); ImGui::SetNextItemWidth(600-40); ImGui::InputText("##cmd1", dummy_str, IM_ARRAYSIZE(dummy_str), ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); ImGui::PushID(358794); - if ( ImGuiToolkit::ButtonIcon(11,2, "Copy to clipboard") ) + if ( ImGuiToolkit::IconButton(ICON_FA_COPY, "Copy to clipboard") ) ImGui::SetClipboardText(dummy_str); ImGui::PopID(); - sprintf(dummy_str, "sudo modprobe v4l2loopback exclusive_caps=1 video_nr=10 card_label=\"vimix loopback\""); - ImGui::Text("Initialize v4l2loopack (after reboot):"); + sprintf(dummy_str, "sudo modprobe v4l2loopback exclusive_caps=1 video_nr=%d" + " card_label=\"vimix loopback\"" , Settings::application.loopback_camera); + ImGui::NewLine(); + ImGui::Text("Initialize v4l2loopack:"); ImGui::SetNextItemWidth(600-40); ImGui::InputText("##cmd2", dummy_str, IM_ARRAYSIZE(dummy_str), ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); ImGui::PushID(899872); - if ( ImGuiToolkit::ButtonIcon(11,2, "Copy to clipboard") ) + if ( ImGuiToolkit::IconButton(ICON_FA_COPY, "Copy to clipboard") ) ImGui::SetClipboardText(dummy_str); ImGui::PopID(); - ImGui::Separator(); + + ImGui::NewLine(); ImGui::SetItemDefaultFocus(); if (ImGui::Button("Ok, I'll do this in a terminal and try again later.", ImVec2(w, 0)) ) { ImGui::CloseCurrentPopup(); } +#endif ImGui::EndPopup(); } -#endif } /// diff --git a/src/UserInterfaceManager.h b/src/UserInterfaceManager.h index f4ecc95..55e5561 100644 --- a/src/UserInterfaceManager.h +++ b/src/UserInterfaceManager.h @@ -111,6 +111,7 @@ class FrameGrabber; class VideoRecorder; class VideoBroadcast; class ShmdataBroadcast; +class Loopback; class SourcePreview { @@ -332,14 +333,11 @@ class OutputPreview : public WorkspaceWindow VideoRecorder *video_recorder_; VideoBroadcast *video_broadcaster_; ShmdataBroadcast *shm_broadcaster_; + Loopback *loopback_broadcaster_; // delayed trigger for recording std::vector< std::future > _video_recorders; -#if defined(LINUX) - FrameGrabber *webcam_emulator_; -#endif - // dialog to select record location DialogToolkit::OpenFolderDialog *recordFolderDialog; @@ -349,11 +347,14 @@ public: void ToggleRecord(bool save_and_continue = false); inline bool isRecording() const { return video_recorder_ != nullptr; } - void ToggleBroadcast(); - inline bool isBroadcasting() const { return video_broadcaster_ != nullptr; } + void ToggleVideoBroadcast(); + inline bool videoBroadcastEnabled() const { return video_broadcaster_ != nullptr; } void ToggleSharedMemory(); - inline bool isSharingMemory() const { return shm_broadcaster_ != nullptr; } + inline bool sharedMemoryEnabled() const { return shm_broadcaster_ != nullptr; } + + bool ToggleLoopbackCamera(); + inline bool loopbackCameraEnabled() const { return loopback_broadcaster_!= nullptr; } void Render(); void setVisible(bool on); diff --git a/src/VideoBroadcast.cpp b/src/VideoBroadcast.cpp index 76043da..c4c493e 100644 --- a/src/VideoBroadcast.cpp +++ b/src/VideoBroadcast.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . **/ +#include #include #include @@ -27,7 +28,6 @@ #include #include "Log.h" -#include "Settings.h" #include "GstToolkit.h" #include "NetworkToolkit.h" @@ -38,14 +38,14 @@ #endif std::string VideoBroadcast::srt_sink_; -std::string VideoBroadcast::h264_encoder_; +std::string VideoBroadcast::srt_encoder_; -std::vector< std::string > pipeline_sink_ { +std::vector< std::string > srt_sink_alternatives_ { "srtsink", "srtserversink" }; -std::vector< std::pair > pipeline_encoder_ { +std::vector< std::pair > srt_encoder_alternatives_ { {"nvh264enc", "nvh264enc zerolatency=true rc-mode=cbr-ld-hq bitrate=4000 ! "}, {"vaapih264enc", "vaapih264enc rate-control=cqp init-qp=26 ! "}, {"x264enc", "x264enc tune=zerolatency ! "} @@ -57,10 +57,10 @@ bool VideoBroadcast::available() static bool _tested = false; if (!_tested) { srt_sink_.clear(); - h264_encoder_.clear(); + srt_encoder_.clear(); - for (auto config = pipeline_sink_.cbegin(); - config != pipeline_sink_.cend() && srt_sink_.empty(); ++config) { + for (auto config = srt_sink_alternatives_.cbegin(); + config != srt_sink_alternatives_.cend() && srt_sink_.empty(); ++config) { if ( GstToolkit::has_feature(*config) ) { srt_sink_ = *config; } @@ -68,11 +68,11 @@ bool VideoBroadcast::available() if (!srt_sink_.empty()) { - for (auto config = pipeline_encoder_.cbegin(); - config != pipeline_encoder_.cend() && h264_encoder_.empty(); ++config) { + for (auto config = srt_encoder_alternatives_.cbegin(); + config != srt_encoder_alternatives_.cend() && srt_encoder_.empty(); ++config) { if ( GstToolkit::has_feature(config->first) ) { - h264_encoder_ = config->second; - if (config->first != pipeline_encoder_.back().first) + srt_encoder_ = config->second; + if (config->first != srt_encoder_alternatives_.back().first) Log::Info("Video Broadcast uses hardware-accelerated encoder (%s)", config->first.c_str()); } } @@ -85,12 +85,14 @@ bool VideoBroadcast::available() } // video broadcast is installed if both srt and h264 are available - return (!srt_sink_.empty() && !h264_encoder_.empty()); + return (!srt_sink_.empty() && !srt_encoder_.empty()); } -VideoBroadcast::VideoBroadcast(int port): FrameGrabber(), port_(port), stopped_(false) +VideoBroadcast::VideoBroadcast(int port): FrameGrabber(), port_(port) { frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, BROADCAST_FPS); // fixed 30 FPS + if (port_ < 1000) + port_ = BROADCAST_DEFAULT_PORT; } std::string VideoBroadcast::init(GstCaps *caps) @@ -106,7 +108,7 @@ std::string VideoBroadcast::init(GstCaps *caps) std::string description = "appsrc name=src ! videoconvert ! "; // complement pipeline with encoder - description += VideoBroadcast::h264_encoder_; + description += VideoBroadcast::srt_encoder_; description += "video/x-h264, profile=high ! queue ! h264parse config-interval=-1 ! mpegtsmux ! "; // complement pipeline with sink @@ -182,8 +184,6 @@ std::string VideoBroadcast::init(GstCaps *caps) return std::string("Video Broadcast started SRT on port ") + std::to_string(port_); } - - void VideoBroadcast::terminate() { // send EOS diff --git a/src/VideoBroadcast.h b/src/VideoBroadcast.h index 4bd92e2..91f9558 100644 --- a/src/VideoBroadcast.h +++ b/src/VideoBroadcast.h @@ -1,20 +1,21 @@ #ifndef VIDEOBROADCAST_H #define VIDEOBROADCAST_H -#include "NetworkToolkit.h" #include "FrameGrabber.h" #include "StreamSource.h" +#define BROADCAST_DEFAULT_PORT 7070 #define BROADCAST_FPS 30 class VideoBroadcast : public FrameGrabber { public: - VideoBroadcast(int port = 7070); + VideoBroadcast(int port = BROADCAST_DEFAULT_PORT); virtual ~VideoBroadcast() {} static bool available(); + inline int port() const { return port_; } void stop() override; std::string info() const override; @@ -25,11 +26,10 @@ private: // connection information int port_; - std::atomic stopped_; // pipeline elements static std::string srt_sink_; - static std::string h264_encoder_; + static std::string srt_encoder_; }; diff --git a/src/defines.h b/src/defines.h index 9797d4e..b80f505 100644 --- a/src/defines.h +++ b/src/defines.h @@ -81,8 +81,8 @@ #define IMGUI_COLOR_LIGHT_OVERLAY IM_COL32(5, 5, 5, 50) #define IMGUI_COLOR_CAPTURE 1.0, 0.55, 0.05 #define IMGUI_COLOR_RECORD 1.0, 0.05, 0.05 -#define IMGUI_COLOR_STREAM 0.05, 0.8, 1.0 -#define IMGUI_COLOR_BROADCAST 0.1, 0.5, 1.0 +#define IMGUI_COLOR_STREAM 0.1, 0.9, 1.0 +#define IMGUI_COLOR_BROADCAST 1.0, 0.9, 0.3 #define IMGUI_NOTIFICATION_DURATION 2.5f #define IMGUI_TOOLTIP_TIMEOUT 80