diff --git a/cfg/video_config.json b/cfg/video_config.json index a324391..288aeba 100644 --- a/cfg/video_config.json +++ b/cfg/video_config.json @@ -5,7 +5,7 @@ { "stream_ip": { - "addr": "239.255.123.123", + "addr": "192.168.1.10", "port": 22202 }, "command_ip": diff --git a/src/basestation/CMakeLists.txt b/src/basestation/CMakeLists.txt index 71e3ae8..33bad81 100644 --- a/src/basestation/CMakeLists.txt +++ b/src/basestation/CMakeLists.txt @@ -7,9 +7,11 @@ set(MODULE_HDR modules/input_config/controller_config.hpp modules/input_config/controller_calibration_popup.hpp modules/input_config/controller_binding_popup.hpp + modules/video_feed_viewer.hpp ) set(MODULE_SRC modules/console.cpp + modules/video_feed_viewer.cpp modules/network_settings.cpp modules/drive_stats.cpp modules/statusbar.cpp @@ -38,34 +40,51 @@ set(WIDGET_SRC widgets/layouts/simple_column.cpp ) -add_executable(basestation - main.cpp - basestation.hpp - basestation.cpp - basestation_screen.hpp - basestation_screen.cpp - controls/controller_manager.hpp - controls/controller_manager.cpp - controls/controller.hpp - controls/controller.cpp - controls/lua_ctrl_lib.hpp - controls/lua_ctrl_lib.cpp - controls/drive_input.hpp - controls/drive_input.cpp - ${MODULE_HDR} - ${MODULE_SRC} - ${WIDGET_HDR} - ${WIDGET_SRC} -) +find_package(PkgConfig) +if (NOT PKG_CONFIG_FOUND) + message(STATUS "basestation: pkg-config unavailable.") +else() + pkg_search_module(PKG_LIBJPEG_TURBO IMPORTED_TARGET libturbojpeg) + + if (NOT PKG_LIBJPEG_TURBO_FOUND) + message(STATUS "basestation: libjpeg-turbo unavailable.") + else() + + find_package(Boost COMPONENTS program_options REQUIRED) -find_package(Boost COMPONENTS program_options REQUIRED) + add_executable(basestation + main.cpp + basestation.hpp + basestation.cpp + basestation_screen.hpp + basestation_screen.cpp + controls/controller_manager.hpp + controls/controller_manager.cpp + controls/controller.hpp + controls/controller.cpp + controls/lua_ctrl_lib.hpp + controls/lua_ctrl_lib.cpp + controls/drive_input.hpp + controls/drive_input.cpp + video_decoder/decoder.hpp + video_decoder/decoder.cpp + ${MODULE_HDR} + ${MODULE_SRC} + ${WIDGET_HDR} + ${WIDGET_SRC} + ) -target_include_directories(basestation PUBLIC nanogui roverlua ${CMAKE_CURRENT_SOURCE_DIR} rover_control Boost::headers) -target_link_libraries(basestation nanogui ${NANOGUI_EXTRA_LIBS} roverlua rover_control Boost::program_options) + target_include_directories(basestation PUBLIC nanogui roverlua network rover_control ${CMAKE_CURRENT_SOURCE_DIR} Boost::headers) + target_link_libraries(basestation nanogui ${NANOGUI_EXTRA_LIBS} roverlua rover_control network PkgConfig::PKG_LIBJPEG_TURBO Boost::program_options) -# NanoGUI requires C++17 -target_compile_features(basestation PUBLIC cxx_std_17) + # NanoGUI requires C++17 + target_compile_features(basestation PUBLIC cxx_std_17) -if (APPLE) - target_link_libraries(basestation "-framework OpenGL") + if (APPLE) + target_link_libraries(basestation "-framework OpenGL") + endif() + + endif() endif() + + diff --git a/src/basestation/basestation.cpp b/src/basestation/basestation.cpp index 68fff98..05d9a93 100644 --- a/src/basestation/basestation.cpp +++ b/src/basestation/basestation.cpp @@ -16,6 +16,7 @@ Basestation::Basestation() : Basestation(boost::property_tree::ptree()) {} Basestation::Basestation(const boost::property_tree::ptree& config) : m_subsystem_sender(main_thread_ctx), m_subsystem_feed(main_thread_ctx), + video_feed_receiver(main_thread_ctx), m_remote_drive(m_subsystem_sender) { if (main_instance != nullptr) { @@ -35,6 +36,14 @@ Basestation::Basestation(const boost::property_tree::ptree& config) m_remote_drive.register_listen_handlers(m_subsystem_feed); m_remote_sensors.register_listen_handlers(m_subsystem_feed); + video_feed_receiver.set_listen_port(22202); + video_feed_receiver.open(); + + //TODO: Add commands or GUI window to open streams. Also do not use 9 fixed like this... + for (int i = 0; i < 9; i++) { + video_feed_receiver.open_stream(i); + } + Console::add_setup_routine([](Console& new_console) { new_console.load_library("ctrl", lua_ctrl_lib::open); new_console.load_library("bs", lua_basestation_lib::open); diff --git a/src/basestation/basestation.hpp b/src/basestation/basestation.hpp index f4f1548..fed44b1 100644 --- a/src/basestation/basestation.hpp +++ b/src/basestation/basestation.hpp @@ -7,11 +7,11 @@ #include #include - #include #include #include #include +#include /* Container class for the main instance of the base station @@ -48,6 +48,9 @@ class Basestation { inline net::MessageReceiver& subsystem_feed() { return m_subsystem_feed; } + inline net::StreamReceiver& video_stream_feed() { + return video_feed_receiver; + } inline DriveInput& remote_drive() { return m_remote_drive; } @@ -78,10 +81,15 @@ class Basestation { static void open(lua_State*); }; + inline void set_video_callback(const std::function& handler) { + video_feed_receiver.on_frame_received(handler); + } + private: boost::asio::io_context main_thread_ctx; net::MessageSender m_subsystem_sender; net::MessageReceiver m_subsystem_feed; + net::StreamReceiver video_feed_receiver; DriveInput m_remote_drive; rc::Sensor m_remote_sensors; diff --git a/src/basestation/basestation_screen.cpp b/src/basestation/basestation_screen.cpp index 159ba59..4a400ab 100644 --- a/src/basestation/basestation_screen.cpp +++ b/src/basestation/basestation_screen.cpp @@ -2,6 +2,7 @@ #include #include +#include #include ScreenPositioning::ScreenPositioning(const nanogui::Vector2i& size, const nanogui::Vector2i& window_pos, int monitor, bool use_fullscreen) : @@ -123,6 +124,10 @@ bool BasestationScreen::keyboard_event(int key, int scancode, int action, int mo } else if (key == GLFW_KEY_N && action == GLFW_PRESS && (mods & GLFW_MOD_CONTROL)) { Basestation::get().add_screen(new BasestationScreen()); handled = true; + } else if (key == GLFW_KEY_C && action == GLFW_PRESS) { + new VideoFeedViewer(this); + Basestation::get().set_video_callback(VideoFeedViewer::update_frame_STATIC); + handled = true; } return handled; diff --git a/src/basestation/modules/network_settings.cpp b/src/basestation/modules/network_settings.cpp index 4cf7f28..333a062 100644 --- a/src/basestation/modules/network_settings.cpp +++ b/src/basestation/modules/network_settings.cpp @@ -34,7 +34,7 @@ gui::NetworkSettings::NetworkSettings(nanogui::Screen* screen) : Section: Subsystem Link settings */ - form->add_group("Subsystem Update Feed"); + form->add_group("Subsystem Update Feed (Incoming)"); auto feed_ip_entry = form->add_variable("Feed IP Address", mcast_feed_str); // This terrifying regex for IP validation was provided by: @@ -100,7 +100,7 @@ gui::NetworkSettings::NetworkSettings(nanogui::Screen* screen) : feed_mcast_mode_box->callback()(subsys_feed_mcast); - subsys_feed_enable = Basestation::get().subsystem_feed().is_multicast(); + subsys_feed_enable = Basestation::get().subsystem_feed().opened(); auto feed_enable_box = form->add_variable("Open", subsys_feed_enable); feed_enable_box->set_callback([this](bool checked) { try { @@ -114,7 +114,11 @@ gui::NetworkSettings::NetworkSettings(nanogui::Screen* screen) : subsys_feed_enable = Basestation::get().subsystem_feed().opened(); }); - form->add_group("Subsystem Device Link"); + /* + Section: Outgoing Subsystem Link + */ + + form->add_group("Subsystem Device Link (Outgoing)"); auto ip_entry = form->add_variable("Device IP Address", ip_str); @@ -180,10 +184,87 @@ gui::NetworkSettings::NetworkSettings(nanogui::Screen* screen) : }); /* - (TODO) Section: Video Link settings - - (This section is a placeholder) + Section: Video Link settings */ + form->add_group("Video Stream Feed (Incoming)"); + + auto stream_ip_entry = form->add_variable("Feed IP Address", video_stream_ip_str); + // This terrifying regex for IP validation was provided by: + // https://stackoverflow.com/a/36760050 + stream_ip_entry->set_format("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$"); + stream_ip_entry->set_value(Basestation::get().video_stream_feed().listen_endpoint().address().to_string()); + stream_ip_entry->set_fixed_width(text_entry_width); + stream_ip_entry->set_callback([this](const std::string& str) { + auto& feed = Basestation::get().video_stream_feed(); + if (str.size() != 0) { + try { + auto ep = feed.listen_endpoint(); + ep.address(boost::asio::ip::address_v4::from_string(str)); + feed.set_listen_endpoint(ep); + return true; + } catch (const std::exception& err) { + open_error_popup(err); + } + } + video_stream_ip_str = feed.listen_endpoint().address().to_string(); + return false; + }); + + video_stream_port = Basestation::get().video_stream_feed().listen_endpoint().port(); + + auto stream_port_entry = form->add_variable("Feed Port", video_stream_port); + // Regex: https://3widgets.com/, range: 0 - 65535 + stream_port_entry->set_format("(\\d|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])"); + stream_port_entry->set_fixed_width(text_entry_width); + stream_port_entry->set_callback([this](int set_port) { + auto& feed = Basestation::get().video_stream_feed(); + try { + if (set_port >= 0 && set_port <= std::numeric_limits().max()) { + auto ep = feed.listen_endpoint(); + ep.port(set_port); + feed.set_listen_endpoint(ep); + return true; + } + } catch (const std::exception& e) { + open_error_popup(e); + } + video_stream_port = feed.listen_endpoint().port(); + return false; + }); + + video_stream_multicast = Basestation::get().video_stream_feed().is_multicast(); + auto stream_mcast_mode_box = form->add_variable("Multicast", video_stream_multicast); + stream_mcast_mode_box->set_callback([this, stream_ip_entry](bool checked) { + auto& feed = Basestation::get().video_stream_feed(); + try { + stream_ip_entry->set_editable(checked); + if (checked) { + stream_ip_entry->set_value(feed.listen_endpoint().address().to_string()); + } else { + stream_ip_entry->set_value(""); + } + feed.set_multicast(checked); + } catch (const std::exception& err) { + open_error_popup(err); + } + video_stream_multicast = feed.is_multicast(); + }); + stream_mcast_mode_box->callback()(video_stream_multicast); + + + video_stream_enable = Basestation::get().video_stream_feed().opened(); + auto stream_enable_box = form->add_variable("Open", video_stream_enable); + stream_enable_box->set_callback([this](bool checked) { + try { + if (checked) + Basestation::get().video_stream_feed().open(); + else + Basestation::get().video_stream_feed().close(); + } catch (const std::exception& err) { + open_error_popup(err); + } + video_stream_enable = Basestation::get().video_stream_feed().opened(); + }); set_position(15); set_visible(true); diff --git a/src/basestation/modules/network_settings.hpp b/src/basestation/modules/network_settings.hpp index 92e41f3..8003d0d 100644 --- a/src/basestation/modules/network_settings.hpp +++ b/src/basestation/modules/network_settings.hpp @@ -14,12 +14,16 @@ class NetworkSettings : public gui::Window { // Most of these variables are for the form helper widgets but aren't really needed std::string ip_str; std::string mcast_feed_str; + std::string video_stream_ip_str; int mcast_feed_port; int port; + int video_stream_port; int interval; bool enable; bool subsys_feed_enable; bool subsys_feed_mcast; + bool video_stream_enable; + bool video_stream_multicast; int text_entry_width = 150; diff --git a/src/basestation/modules/statusbar.cpp b/src/basestation/modules/statusbar.cpp index 4c6f7ec..b08eec9 100644 --- a/src/basestation/modules/statusbar.cpp +++ b/src/basestation/modules/statusbar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include gui::Statusbar::Statusbar(nanogui::Widget* parent) : gui::Toolbar(parent) { { @@ -20,6 +21,12 @@ gui::Statusbar::Statusbar(nanogui::Widget* parent) : gui::Toolbar(parent) { auto wnd = new ControllerConfig(screen(), Basestation::get().controller_manager()); wnd->center(); }); + auto video_player_button = new nanogui::ToolButton(left_tray(), FA_VIDEO); + video_player_button->set_flags(nanogui::Button::NormalButton); + video_player_button->set_callback([this] { + auto wnd = new VideoFeedViewer(screen()); + wnd->center(); + }); } { auto drive_button = new nanogui::ToolButton(right_tray(), FA_COGS); diff --git a/src/basestation/modules/video_feed_viewer.cpp b/src/basestation/modules/video_feed_viewer.cpp new file mode 100644 index 0000000..5ddaa5d --- /dev/null +++ b/src/basestation/modules/video_feed_viewer.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include +#include + +VideoFeedViewer* vid_feed = nullptr; + +nanogui::Vector2i VideoFeedViewer::preferred_size(NVGcontext* ctx) const { + return nanogui::Vector2i( + m_fixed_size.x() ? m_fixed_size.x() : m_size.x(), + m_fixed_size.y() ? m_fixed_size.y() : m_size.y() + ); +} + +VideoFeedViewer::VideoFeedViewer(nanogui::Widget* parent) : + gui::Window(parent, "Rover Feed", true), + decoder() +{ + vid_feed = this; + + set_layout(new gui::SimpleColumnLayout(MARGIN, MARGIN, 6, gui::SimpleColumnLayout::HorizontalAnchor::STRETCH)); + + video_widget = new nanogui::ImageView(this); + video_widget->set_border_color(nanogui::Color(0, 0, 0, 0)); + + set_size(nanogui::min(nanogui::Vector2i(1280, 720), parent->size() - 100)); + parent->perform_layout(screen()->nvg_context()); + center(); +} + +bool VideoFeedViewer::keyboard_event(int key, int scancode, int action, int modifiers) { + if (gui::Window::keyboard_event(key, scancode, action, modifiers)) { + return true; + } + bool handled = false; + + if (action == GLFW_PRESS && !(modifiers & (GLFW_MOD_CONTROL))) { + handled = true; + switch (key) { + case GLFW_KEY_F: + borderless = !borderless; + if (gui::SimpleColumnLayout* layout = dynamic_cast(m_layout.get())) { + if (borderless) { + layout->set_horizontal_margin(0); + layout->set_vertical_margin(0); + } else { + layout->set_horizontal_margin(MARGIN); + layout->set_vertical_margin(MARGIN); + } + perform_layout(screen()->nvg_context()); + } + break; + case GLFW_KEY_T: { + // Fit the image in the current size + fit_image(); + break; + } + case GLFW_KEY_Y: { + // Resize window to fit the feed + nanogui::Vector2i ideal_size = video_widget->image()->size(); + if (!borderless) { + ideal_size += MARGIN; + } + ideal_size.y() += m_theme->m_window_header_height; + + auto scr = screen(); + set_size(nanogui::min(ideal_size, scr->size() - 100)); + perform_layout(scr->nvg_context()); + + fit_image(); + break; + } + default: + handled = false; + break; + } + } + + return handled; +} + +void VideoFeedViewer::fit_image() { + nanogui::Vector2f scale = static_cast(video_widget->size()) / static_cast(video_widget->image()->size()); + video_widget->set_scale(std::min(scale.x(), scale.y())); + video_widget->center(); +} + +void VideoFeedViewer::update_frame_STATIC(int stream, net::Frame& frame){ + vid_feed->update_frame(stream, frame); +} + +void VideoFeedViewer::update_frame(int stream, net::Frame& frame) { + uint8_t next_frame_buffer[Decoder::CAMERA_HEIGHT*Decoder::CAMERA_WIDTH*3]; + + try { + decoder.decode_frame(frame, next_frame_buffer); + } + catch(std::runtime_error& e) { + std::cerr << e.what() << std::endl; + return; + } + + nanogui::Texture* next_frame = new nanogui::Texture( + nanogui::Texture::PixelFormat::RGB, + nanogui::Texture::ComponentFormat::UInt8, + nanogui::Vector2i(Decoder::CAMERA_WIDTH,Decoder::CAMERA_HEIGHT), + nanogui::Texture::InterpolationMode::Bilinear, + nanogui::Texture::InterpolationMode::Nearest, + nanogui::Texture::WrapMode::ClampToEdge, + (uint8_t) 1, + nanogui::Texture::TextureFlags::ShaderRead, + false + ); + + next_frame->upload((uint8_t *) next_frame_buffer); + video_widget->set_image(next_frame); +} + diff --git a/src/basestation/modules/video_feed_viewer.hpp b/src/basestation/modules/video_feed_viewer.hpp new file mode 100644 index 0000000..6212afa --- /dev/null +++ b/src/basestation/modules/video_feed_viewer.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +class VideoFeedViewer : public gui::Window { + public: + VideoFeedViewer(nanogui::Widget* parent); + static void update_frame_STATIC(int stream, net::Frame& frame); + void update_frame(int stream, net::Frame& frame); + + virtual nanogui::Vector2i preferred_size(NVGcontext* ctx) const override; + virtual bool keyboard_event(int key, int scancode, int action, int modifiers) override; + + protected: + nanogui::ImageView* video_widget; + private: + constexpr static int MARGIN = 4; + Decoder decoder; + bool borderless = false; + + void fit_image(); +}; diff --git a/src/basestation/video_decoder/decoder.cpp b/src/basestation/video_decoder/decoder.cpp new file mode 100644 index 0000000..ecdcfea --- /dev/null +++ b/src/basestation/video_decoder/decoder.cpp @@ -0,0 +1,27 @@ +#include + +Decoder::Decoder() { + jpeg_decompressor = tjInitDecompress(); +} + +Decoder::~Decoder() { + tjDestroy(jpeg_decompressor); +} + +void Decoder::decode_frame( net::Frame& frame, uint8_t* out_buffer) { + if (tjDecompress2( + jpeg_decompressor, + frame.data(), + (long unsigned int)frame.size(), + out_buffer, + CAMERA_WIDTH, + 3 * CAMERA_WIDTH, + CAMERA_HEIGHT, + TJPF_RGB, + TJFLAG_NOREALLOC + )) { + + out_buffer = nullptr; + throw std::runtime_error(tjGetErrorStr()); + } +} diff --git a/src/basestation/video_decoder/decoder.hpp b/src/basestation/video_decoder/decoder.hpp new file mode 100644 index 0000000..d5edf6e --- /dev/null +++ b/src/basestation/video_decoder/decoder.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class Decoder { + private: + tjhandle jpeg_decompressor; + public: + Decoder(); + ~Decoder(); + void decode_frame(net::Frame& frame, unsigned char * out_buffer); + + // TODO: Retrieve individually for each stream + constexpr static unsigned int CAMERA_WIDTH = 1280; + constexpr static unsigned int CAMERA_HEIGHT = 720; +}; \ No newline at end of file diff --git a/src/network/stream.cpp b/src/network/stream.cpp index 0ffa6e5..87cf147 100644 --- a/src/network/stream.cpp +++ b/src/network/stream.cpp @@ -154,36 +154,64 @@ net::StreamReceiver::StreamReceiver(boost::asio::io_context& io_context) : ctx(i streams.reserve(8); } -void net::StreamReceiver::set_listen_port(uint16_t port) { - this->port = port; - if (socket.is_open()) { +void net::StreamReceiver::open() { + if (socket.is_open()) socket.close(); - begin(port); + + if (use_multicast) { + socket.open(listen_ep.protocol()); + socket.set_option(boost::asio::ip::udp::socket::reuse_address(true)); + socket.bind(boost::asio::ip::udp::endpoint( + boost::asio::ip::address_v4::from_string("0.0.0.0"), + listen_ep.port() + )); + socket.set_option(boost::asio::ip::multicast::join_group(listen_ep.address())); + } else { + socket.open(listen_ep.protocol()); + socket.bind(boost::asio::ip::udp::endpoint(listen_ep.protocol(), listen_ep.port())); } + receive(); } -void net::StreamReceiver::begin(uint16_t port) { - socket.open(boost::asio::ip::udp::v4()); - socket.bind(boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), port)); +void net::StreamReceiver::close() { + if (socket.is_open()) + socket.close(); +} - receive(); +void net::StreamReceiver::set_multicast(bool on) { + if (on != use_multicast) { + use_multicast = on; + if (socket.is_open()) { + open(); + } + } } -void net::StreamReceiver::subscribe(boost::asio::ip::udp::endpoint& ep) { +void net::StreamReceiver::set_listen_port(uint_least16_t port) { + use_multicast = false; + listen_ep.port(port); + if (socket.is_open()) { - socket.close(); + open(); } + +} - socket.open(ep.protocol()); - socket.set_option(boost::asio::ip::udp::socket::reuse_address(true)); - socket.bind(boost::asio::ip::udp::endpoint( - boost::asio::ip::address_v4::from_string("0.0.0.0"), - ep.port() - )); +void net::StreamReceiver::subscribe(const boost::asio::ip::udp::endpoint& ep) { + use_multicast = true; + listen_ep = ep; - socket.set_option(boost::asio::ip::multicast::join_group(ep.address())); + if (socket.is_open()) { + open(); + } +} - receive(); +void net::StreamReceiver::set_listen_endpoint(const boost::asio::ip::udp::endpoint& ep) { + listen_ep = ep; + + if (socket.is_open()) { + open(); + } } void net::StreamReceiver::receive() { @@ -242,7 +270,12 @@ void net::StreamReceiver::receive() { } } - receive(); + // If the socket was closed, silently exit + if (error != boost::asio::error::operation_aborted) { + if (error) + m_error_emitter(error); + receive(); + } }); } @@ -300,7 +333,7 @@ void net::StreamReceiver::set_section_buffer_size(std::size_t size) { recv_buffer.reset(new uint8_t[recv_buffer_size]); if (reopen) - begin(port); + open(); } diff --git a/src/network/stream.hpp b/src/network/stream.hpp index da9f366..6bdb59b 100644 --- a/src/network/stream.hpp +++ b/src/network/stream.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace net { @@ -135,12 +136,25 @@ class StreamReceiver { StreamReceiver(boost::asio::io_context& io_context); - // Regular Mode - void set_listen_port(uint16_t port); - void begin(uint16_t port); + // Set the listen port and turn multicast off + void set_listen_port(uint_least16_t port); - // Multicast Mode - void subscribe(boost::asio::ip::udp::endpoint& feed); + // Set the listen endpoint and enable multicast + void subscribe(const boost::asio::ip::udp::endpoint& mcast_feed); + + // Set the listen endpoint but do not change multicast setting + void set_listen_endpoint(const boost::asio::ip::udp::endpoint&); + + void set_multicast(bool on); + + inline bool is_multicast() const { return use_multicast; } + inline int listen_port() const { return listen_ep.port(); } + inline const boost::asio::ip::udp::endpoint& listen_endpoint() const { return listen_ep; } + + void open(); + void close(); + inline bool opened() const { return socket.is_open(); } + inline event::Emitter& receive_error_emitter() { return m_error_emitter; } // Open stream and allocate buffers if needed void open_stream(int stream); @@ -172,9 +186,14 @@ class StreamReceiver { boost::asio::io_context& ctx; boost::asio::ip::udp::socket socket; boost::asio::ip::udp::endpoint remote; + boost::asio::ip::udp::endpoint listen_ep; + event::Emitter m_error_emitter; + bool use_multicast = false; + std::unique_ptr recv_buffer; // start with default size; allocates on write std::size_t recv_buffer_size = 2048; + std::vector streams; // Reader-writer lock: stream vector cannot be reallocated while being read std::shared_mutex streams_lock; @@ -182,7 +201,7 @@ class StreamReceiver { // Default size: 4MB unsigned _frame_buffer_size = 4 * 1024 * 1024; unsigned _frame_buffer_level = 3; - uint16_t port; + void receive(); };