From 77915210ccca791086d2f290d7bb04f89ff5c773 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Fri, 12 Sep 2025 18:16:31 +0800 Subject: [PATCH 01/36] add camera parsing from cli to library add loop option to recording --- tool/tvcli.cc | 86 +++++++--------------------------------- xdaqvc/camera.cc | 95 ++++++++++++++++++++++++++++++++------------- xdaqvc/camera.h | 75 ++++++++++++++++++++++++++++------- xdaqvc/ws_client.cc | 52 +++++++++++++++++++++---- xdaqvc/ws_client.h | 9 ++++- xdaqvc/xvc.cc | 87 +++++++++++++++++------------------------ xdaqvc/xvc.h | 4 +- 7 files changed, 232 insertions(+), 176 deletions(-) diff --git a/tool/tvcli.cc b/tool/tvcli.cc index d39dd33..a7ef62e 100644 --- a/tool/tvcli.cc +++ b/tool/tvcli.cc @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -21,8 +20,6 @@ #include "xdaqmetadata/metadata_handler.h" #include "xvc.h" -using json = nlohmann::json; - namespace { @@ -100,68 +97,6 @@ void handle_sigint(int) std::exit(EXIT_SUCCESS); } -std::string cap_to_string(const Camera::Cap &cap) -{ - // Skip format for image/jpeg media type - if (cap.format.empty()) { - return fmt::format( - "{},width={},height={},framerate={}/{}", - cap.media_type, - cap.width, - cap.height, - cap.fps_n, - cap.fps_d - ); - } else { - return fmt::format( - "{},format={},width={},height={},framerate={}/{}", - cap.media_type, - cap.format, - cap.width, - cap.height, - cap.fps_n, - cap.fps_d - ); - } -} - -std::vector cameras() -{ - auto const cameras_str = Camera::cameras(); - std::vector cams; - - if (cameras_str.empty()) { - fmt::println("No camera found"); - return cams; - } - - auto const cameras_json = json::parse(cameras_str); - - for (const auto &camera_json : cameras_json) { - auto id = camera_json["id"].get(); - auto name = camera_json["name"].get(); - auto cam = new Camera(id, name); - - for (const auto &cap_json : camera_json["caps"]) { - Camera::Cap cap; - cap.media_type = cap_json["media_type"].get(); - cap.format = cap_json["format"].get(); - cap.width = cap_json["width"].get(); - cap.height = cap_json["height"].get(); - - auto framerate_str = cap_json["framerate"].get(); - auto delimiter_pos = framerate_str.find('/'); - if (delimiter_pos != std::string::npos) { - cap.fps_n = std::stoi(framerate_str.substr(0, delimiter_pos)); - cap.fps_d = std::stoi(framerate_str.substr(delimiter_pos + 1)); - } - cam->add_cap(cap); - } - cams.emplace_back(cam); - } - return cams; -} - } // namespace int func(int argc, char *argv[]) @@ -255,7 +190,7 @@ int func(int argc, char *argv[]) } if (!test) { - cams = cameras(); + cams = Camera::cameras(); for (auto cam : cams) { if (id == cam->id()) { stream_cam = cam; @@ -269,20 +204,25 @@ int func(int argc, char *argv[]) auto caps = stream_cam->caps(); auto it = std::find_if(caps.begin(), caps.end(), [cap](const Camera::Cap &_cap) { - return cap_to_string(_cap) == cap; + return _cap.to_string() == cap; }); if (it == caps.end()) { fmt::println("Error: Camera {} does not support cap '{}'", id, cap); return EXIT_FAILURE; } + stream_cam->set_test(test); + stream_cam->start(*it); } else { stream_cam = new Camera(id, "test"); + stream_cam->set_test(test); + stream_cam->start(Camera::Cap{ + .media_type = "image/jpeg", + .width = 1920, + .height = 1080, + .fps_n = 30, + }); } - stream_cam->set_current_cap(cap); - stream_cam->set_test(test); - stream_cam->start(); - auto uri = fmt::format("{}:{}", host, stream_cam->port()); auto record_path = std::filesystem::current_path(); auto filepath = record_path / fmt::format("{}-{}", stream_cam->name(), stream_cam->id()); @@ -338,7 +278,7 @@ int func(int argc, char *argv[]) } if (*list) { - cams = cameras(); + cams = Camera::cameras(); fmt::println("Discovered Cameras:"); for (auto cam : cams) { @@ -348,7 +288,7 @@ int func(int argc, char *argv[]) fmt::println("Capabilities :"); for (auto cap : cam->caps()) { - fmt::println(" - {}", cap_to_string(cap)); + fmt::println(" - {}", cap.to_string()); } } } diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index b660aa2..c2c60aa 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -3,11 +3,11 @@ #include #include -#include +// #include #include "port_pool.h" -using nlohmann::json; +// using nlohmann::json; namespace { @@ -19,21 +19,17 @@ auto constexpr H265 = "192.168.177.100:8000/h265"; auto constexpr Stop = "192.168.177.100:8000/stop"; auto constexpr OK = 200; -auto constexpr VIDEO_MJPEG = "image/jpeg"; -auto constexpr VIDEO_RAW = "video/x-raw"; - PortPool pool(9000, 9064); } // namespace -Camera::Camera(const int id, const std::string &name) - : _id(id), _name(name), _current_cap(""), _test(false) +Camera::Camera(const int id, const std::string &name) : _id(id), _name(name), _test(false) { auto port = pool.allocate_port(); if (port) { _port = port.value(); } - spdlog::info("Creating camera id: {}, name: {}, port: {}", id, name, _port); + spdlog::info("Creating camera id: {}, name: {}, port: {}", _id, _name, _port); } Camera::~Camera() @@ -42,32 +38,80 @@ Camera::~Camera() spdlog::info("Deleting camera id: {}, name: {}, port: {}", _id, _name, _port); } -std::string Camera::cameras(const std::chrono::milliseconds duration) +// std::vector> Camera::cameras(const std::chrono::milliseconds duration) +std::vector Camera::cameras(const std::chrono::milliseconds duration) { - auto cameras = std::string(""); + // std::vector> cameras; + std::vector cameras; auto response = cpr::Get(cpr::Url(Cameras), cpr::Timeout(duration)); - if (response.status_code == OK) { - cameras = json::parse(response.text).dump(2); + if (response.status_code != OK) { + spdlog::warn("Failed to fetch cameras."); + return cameras; + } + + try { + auto cameras_json = json::parse(response.text); + for (const auto &cam : cameras_json) { + cameras.emplace_back(Camera::parse(cam)); + } + } catch (const std::exception &e) { + spdlog::error("JSON parse error: {}", e.what()); } + return cameras; } -void Camera::start(const std::chrono::milliseconds duration) +// std::unique_ptr Camera::parse(const json &camera_json) +Camera *Camera::parse(const json &camera_json) +{ + auto const id = camera_json["id"].get(); + auto const name = camera_json["name"].get(); + auto const caps_json = camera_json["caps"]; + + auto camera = new Camera(id, name); + // auto camera = std::make_unique(id, name); + + for (const auto &cap_json : caps_json) { + Camera::Cap cap; + cap.media_type = cap_json.at("media_type").get(); + cap.format = cap_json.at("format").get(); + cap.width = cap_json.at("width").get(); + cap.height = cap_json.at("height").get(); + + auto framerate_str = cap_json.at("framerate").get(); + auto delimiter_pos = framerate_str.find('/'); + if (delimiter_pos != std::string::npos) { + cap.fps_n = std::stoi(framerate_str.substr(0, delimiter_pos)); + cap.fps_d = std::stoi(framerate_str.substr(delimiter_pos + 1)); + } + camera->add_cap(cap); + + // TODO: hack + if (cap.media_type == "image/jpeg") { + camera->add_codec(Codec::MJPEG); + } else if (cap.media_type == "video/x-raw") { + camera->add_codec(Codec::MJPEG); + camera->add_codec(Codec::H265); + } + } + + return camera; +} + +void Camera::start(const Cap &cap, const std::chrono::milliseconds duration) { json payload; payload["id"] = _id; - payload["capability"] = _current_cap; + payload["capability"] = cap.to_string(); payload["port"] = _port; - cpr::Url url; + cpr::Url url; if (_test) { url = cpr::Url(test); - } else if (_current_cap.find(VIDEO_MJPEG) != std::string::npos || - _current_cap.find(VIDEO_RAW) != std::string::npos) { + } else if (_stream_codec == Codec::MJPEG) { url = cpr::Url(jpeg); - } else { - // TODO: disable h265 for now + } else if (_stream_codec == Codec::H265) { url = cpr::Url(H265); } @@ -79,14 +123,13 @@ void Camera::start(const std::chrono::milliseconds duration) ); if (response.status_code == OK) { spdlog::info( - "Successfully send http request to start camera: {} with id: {}, port: {}, cap: {}", - jpeg, + "Successfully send HTTP request to start camera, id: {}, port: {}, cap: {}", _id, _port, - _current_cap + cap.to_string() ); } else { - spdlog::info("Failed to start camera"); + spdlog::warn("Failed to start camera, status code: {}", response.status_code); } } @@ -102,8 +145,8 @@ void Camera::stop(const std::chrono::milliseconds duration) cpr::Timeout(duration) ); if (response.status_code == OK) { - spdlog::info("Successfully send http request to stop camera: {} with id: {}", Stop, _id); + spdlog::info("Successfully send HTTP request to stop camera, id: {}", _id); } else { - spdlog::info("Failed to stop camera"); + spdlog::warn("Failed to stop camera, status code: {}", response.status_code); } -} +} \ No newline at end of file diff --git a/xdaqvc/camera.h b/xdaqvc/camera.h index 3692485..2c33cdb 100644 --- a/xdaqvc/camera.h +++ b/xdaqvc/camera.h @@ -1,49 +1,96 @@ #pragma once +#include + #include +#include +#include #include #include - using namespace std::chrono_literals; - +using nlohmann::json; class Camera { public: + // video/x-raw,format=YUY2,width=640,height=480,framerate=30/1 + // image/jpeg,width=640,height=480,framerate=30/1 struct Cap { std::string media_type; - std::string format; + std::optional format = std::nullopt; int width; int height; int fps_n; int fps_d; + + std::string to_string() const + { + if (format.has_value() && !format.value().empty()) { + return fmt::format( + "{},format={},width={},height={},framerate={}/{}", + media_type, + format.value(), + width, + height, + fps_n, + fps_d + ); + } else { + return fmt::format( + "{},width={},height={},framerate={}/{}", media_type, width, height, fps_n, fps_d + ); + } + } }; + enum class Codec { MJPEG, H265, H264 }; - Camera(const int id, const std::string &name); + Camera(const int id = -1, const std::string &name = ""); ~Camera(); - [[nodiscard]] static std::string cameras(const std::chrono::milliseconds duration = 500ms); - [[nodiscard]] std::string name() const { return _name; }; - [[nodiscard]] std::vector caps() const { return _caps; }; - [[nodiscard]] unsigned short port() const { return _port; }; + // [[nodiscard]] static std::unique_ptr parse(const json &event); + [[nodiscard]] static Camera *parse(const json &event); + + // [[nodiscard]] static std::vector> cameras( + // const std::chrono::milliseconds duration = 500ms + // ); + [[nodiscard]] static std::vector cameras( + const std::chrono::milliseconds duration = 500ms + ); [[nodiscard]] int id() const { return _id; } - [[nodiscard]] std::string current_cap() const { return _current_cap; }; + [[nodiscard]] unsigned short port() const { return _port; } - void set_current_cap(const std::string &cap) { _current_cap = cap; } + [[nodiscard]] std::string name() const { return _name; } + void set_name(const std::string &name) { _name = name; } + + [[nodiscard]] std::vector caps() const { return _caps; } void add_cap(const Cap &cap) { _caps.emplace_back(cap); } - void start(const std::chrono::milliseconds duration = 500ms); + [[nodiscard]] std::vector codecs() const { return _codecs; } + void add_codec(const Codec &codec) + { + if (std::find(_codecs.begin(), _codecs.end(), codec) == _codecs.end()) { + _codecs.emplace_back(codec); + } + } + + [[nodiscard]] Codec stream_codec() const { return _stream_codec; } + void set_stream_codec(const Codec &codec) { _stream_codec = codec; } + + void start(const Cap &cap, std::chrono::milliseconds duration = 500ms); void stop(const std::chrono::milliseconds duration = 500ms); - void set_test(const bool test) { _test = test; }; - [[nodiscard]] bool test_mode() const { return _test; }; + [[nodiscard]] bool test_mode() const { return _test; } + void set_test(const bool test) { _test = test; } private: int _id; unsigned short _port; std::string _name; + std::vector _caps; - std::string _current_cap; + std::vector _codecs; + Codec _stream_codec; + bool _test; }; \ No newline at end of file diff --git a/xdaqvc/ws_client.cc b/xdaqvc/ws_client.cc index a37ca0f..ef8414c 100644 --- a/xdaqvc/ws_client.cc +++ b/xdaqvc/ws_client.cc @@ -2,6 +2,7 @@ #include +#include #include #include @@ -33,8 +34,11 @@ session::session(net::io_context &ioc, std::function handler) { } +void session::request_stop() { _stopping.store(true, std::memory_order_relaxed); } + void session::run(char const *host, char const *port) { + if (_stopping.load(std::memory_order_relaxed)) return; _host = host; // Look up the domain name @@ -45,6 +49,7 @@ void session::run(char const *host, char const *port) void session::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (_stopping.load(std::memory_order_relaxed)) return; if (ec) return fail(ec, RESOLVE); // Set the timeout for the operation @@ -58,6 +63,7 @@ void session::on_resolve(beast::error_code ec, tcp::resolver::results_type resul void session::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) { + if (_stopping.load(std::memory_order_relaxed)) return; if (ec) { fail(ec, CONNECT); reconnect(); @@ -92,6 +98,7 @@ void session::on_connect(beast::error_code ec, tcp::resolver::results_type::endp void session::on_handshake(beast::error_code ec) { + if (_stopping.load(std::memory_order_relaxed)) return; if (ec) return fail(ec, HANDSHAKE); read(); @@ -99,12 +106,14 @@ void session::on_handshake(beast::error_code ec) void session::read() { + if (_stopping.load(std::memory_order_relaxed)) return; _ws.async_read(_buffer, beast::bind_front_handler(&session::on_read, shared_from_this())); } void session::on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); + if (_stopping.load(std::memory_order_relaxed)) return; if (ec) { fail(ec, READ); @@ -126,10 +135,19 @@ void session::on_read(beast::error_code ec, std::size_t bytes_transferred) void session::close() { // Close the WebSocket connection - _ws.async_close( - websocket::close_code::normal, - beast::bind_front_handler(&session::on_close, shared_from_this()) - ); + // _ws.async_close( + // websocket::close_code::normal, + // beast::bind_front_handler(&session::on_close, shared_from_this()) + // ); + net::dispatch(_ws.get_executor(), [self = shared_from_this()] { + if (self->_stopping.load(std::memory_order_relaxed)) return; + self->_stopping.store(true, std::memory_order_relaxed); + if (self->_ws.is_open()) { + self->_ws.async_close( + websocket::close_code::normal, beast::bind_front_handler(&session::on_close, self) + ); + } + }); } void session::on_close(beast::error_code ec) @@ -143,6 +161,7 @@ void session::on_close(beast::error_code ec) void session::reconnect(const std::chrono::milliseconds timeout) { + if (_stopping.load(std::memory_order_relaxed)) return; spdlog::debug("session has been disconnected, trying to reconnect..."); if (_ws.is_open()) { @@ -151,6 +170,13 @@ void session::reconnect(const std::chrono::milliseconds timeout) spdlog::debug("next trial will start after {}ms", timeout.count()); std::this_thread::sleep_for(timeout); + // auto timer = std::make_shared(_ws.get_executor()); + // timer->expires_after(timeout); + // timer->async_wait([self = shared_from_this(), timer](beast::error_code ec) { + // if (ec) return; // Timer was cancelled + + // self->run("192.168.177.100", "8000"); + // }); auto const host = "192.168.177.100"; auto const port = "8000"; @@ -180,12 +206,24 @@ ws_client::ws_client(std::function handler) : _event_handler( spdlog::error("WebSocket thread error: {}", e.what()); } }); + + // net::signal_set signals(*_ioc, SIGINT, SIGTERM); + // signals.async_wait([this](const boost::system::error_code &, int) { _ioc->stop(); }); } -ws_client::~ws_client() +ws_client::~ws_client() { shutdown(); } + +void ws_client::shutdown() { - // _ioc->stop(); - // _session->close(); + spdlog::info("ws_client::shutdown"); + + if (_session) { + _session->request_stop(); + _session->close(); + } + if (_ioc) { + _ioc->stop(); + } } } // namespace xvc \ No newline at end of file diff --git a/xdaqvc/ws_client.h b/xdaqvc/ws_client.h index 2fd5f9e..306ab46 100644 --- a/xdaqvc/ws_client.h +++ b/xdaqvc/ws_client.h @@ -34,6 +34,8 @@ class session : public std::enable_shared_from_this std::string _host; std::function _event_handler; + std::atomic _stopping{false}; + public: // Resolver and socket require an io_context explicit session(net::io_context &ioc, std::function handler); @@ -46,6 +48,9 @@ class session : public std::enable_shared_from_this void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep); void on_handshake(beast::error_code ec); + // tell the session to stop gracefully (no more reconnects) + void request_stop(); + void read(); void on_read(beast::error_code ec, std::size_t bytes_transferred); void close(); @@ -57,9 +62,11 @@ class session : public std::enable_shared_from_this class ws_client { public: - ws_client(std::function handler); + ws_client(std::function handler = nullptr); ~ws_client(); + void shutdown(); + private: std::shared_ptr _session; std::unique_ptr _ioc; diff --git a/xdaqvc/xvc.cc b/xdaqvc/xvc.cc index 256a97e..5df6a1d 100644 --- a/xdaqvc/xvc.cc +++ b/xdaqvc/xvc.cc @@ -24,6 +24,8 @@ #include #include +#include +#include #include #include #include @@ -31,10 +33,8 @@ #include "xdaqmetadata/key_value_store.h" #include "xdaqmetadata/xdaqmetadata.h" - using namespace std::chrono_literals; - namespace { @@ -90,7 +90,6 @@ gchararray generate_filename( } // namespace - namespace xvc { @@ -176,6 +175,10 @@ void setup_h265_srt_stream(GstPipeline *pipeline, const std::string &uri) void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri) { + if (!pipeline) { + spdlog::error("Pipeline is null"); + return; + } spdlog::info("Setup GStreamer M-JPEG SRT stream pipeline with uri: {}", uri); auto src = create_element("srtclientsrc", "src"); @@ -231,50 +234,6 @@ void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri) } } -void decode_toggle(GstPipeline *pipeline, bool decode) -{ - std::unique_ptr queue_display( - gst_bin_get_by_name(GST_BIN(pipeline), "queue_display"), gst_object_unref - ); - if (!queue_display) { - spdlog::error("Failed to find element: 'queue_display'"); - return; - } - - std::unique_ptr src_pad( - gst_element_get_static_pad(queue_display.get(), "src"), gst_object_unref - ); - if (!src_pad) { - spdlog::error("Failed to get src pad from 'queue_display'"); - return; - } - - static unsigned long probe_id = 0; - - if (decode) { - spdlog::debug( - "Remove probe from src pad on 'queue_display' to allow buffers to pass through" - ); - // TODO: 'decode' defaults to true, this line generate a warning - gst_pad_remove_probe(src_pad.get(), probe_id); - - } else { - spdlog::debug("Add probe to src pad on 'queue_display' to drop buffers"); - probe_id = gst_pad_add_probe( - src_pad.get(), - GST_PAD_PROBE_TYPE_BUFFER, - []([[maybe_unused]] GstPad *pad, - [[maybe_unused]] GstPadProbeInfo *info, - [[maybe_unused]] gpointer user_data) -> GstPadProbeReturn { - spdlog::trace("Drop buffer before decode"); - return GST_PAD_PROBE_DROP; - }, - nullptr, - nullptr - ); - } -} - void start_h265_recording( GstPipeline *pipeline, fs::path &filepath, bool continuous, int max_size_time, int max_files ) @@ -415,10 +374,30 @@ void stop_h265_recording(GstPipeline *pipeline) } void start_jpeg_recording( - GstPipeline *pipeline, fs::path &filepath, bool continuous, int max_size_time, TimeUnit unit, - int max_files + GstPipeline *pipeline, fs::path &filepath, bool split, int max_size_time, TimeUnit unit, + bool loop, int max_files ) { + if (!pipeline) { + spdlog::error("Pipeline is null"); + return; + } + if (filepath.empty()) { + spdlog::error("filepath is empty"); + return; + } + + auto path = filepath.parent_path(); + if (!fs::exists(path)) { + spdlog::info("Create Directory: {}", path.generic_string()); + std::error_code ec; + if (!fs::create_directories(path, ec)) { + spdlog::info( + "Failed to create directory: {}. Error: {}", path.generic_string(), ec.message() + ); + } + } + spdlog::info("Start GStreamer M-JPEG recording"); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); @@ -436,6 +415,8 @@ void start_jpeg_recording( default: break; } + max_files = loop ? max_files : INT_MAX; + auto tracker = std::make_unique(FileTracker{filepath.generic_string(), {}, max_files}); @@ -446,15 +427,13 @@ void start_jpeg_recording( g_object_set(G_OBJECT(muxer), "offset-to-zero", true, nullptr); g_object_set( G_OBJECT(filesink), - "max-size-time", continuous ? 0 : max_size_time * GST_SECOND, // max-size-time=0 -> continuous - "max-files", max_files, + "max-size-time", split ? max_size_time * GST_SECOND : 0, // max-size-time=0 -> continuous "async-finalize", false, "muxer", muxer, nullptr ); // clang-format on - gst_bin_add_many(GST_BIN(pipeline), queue_record, parser, filesink, nullptr); if (!gst_element_link_many(queue_record, parser, filesink, nullptr)) { @@ -482,6 +461,10 @@ void start_jpeg_recording( void stop_jpeg_recording(GstPipeline *pipeline) { + if (!pipeline) { + spdlog::error("Pipeline is null"); + return; + } spdlog::info("Stop GStreamer M-JPEG recording"); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index 8cdfd1b..a9d7636 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -17,8 +17,6 @@ enum class TimeUnit { Seconds = 0, Minutes, Hours, Days }; void setup_h265_srt_stream(GstPipeline *pipeline, const std::string &uri); void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri); -void decode_toggle(GstPipeline *pipeline, bool decode = true); - void mock_camera( GstPipeline *pipeline, [[maybe_unused]] const std::string &uri, const std::string ¤t_cap ); @@ -30,7 +28,7 @@ void stop_h265_recording(GstPipeline *pipeline); void start_jpeg_recording( GstPipeline *pipeline, fs::path &filepath, bool continuous = true, int max_size_time = 10, - TimeUnit unit = TimeUnit::Minutes, int max_files = 10 + TimeUnit unit = TimeUnit::Minutes, bool loop = true, int max_files = 10 ); void stop_jpeg_recording(GstPipeline *pipeline); From cbd1bd11378e60b192e0701adb0b63df292a2271 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 16 Oct 2025 11:50:01 +0800 Subject: [PATCH 02/36] fix: ownership of io context in websocket client --- CMakeLists.txt | 4 +- conanfile.py | 2 +- test/CMakeLists.txt | 19 +++++- test/test_ws_client.cc | 28 ++++++++ xdaqvc/ws_client.cc | 146 +++++++++++++---------------------------- xdaqvc/ws_client.h | 39 ++++++----- 6 files changed, 113 insertions(+), 125 deletions(-) create mode 100644 test/test_ws_client.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 040a60b..9b92fcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,11 +2,13 @@ cmake_minimum_required(VERSION 3.25) set(libxvc_VERSION 0.1.0) +set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum OS X deployment version" FORCE) + project(libxvc LANGUAGES CXX VERSION "${libxvc_VERSION}" DESCRIPTION "Thor Vision Video Capture Library" - HOMEPAGE_URL "https://www.kontex.io/" + HOMEPAGE_URL "https://github.com/kontex-neuro/libxvc" ) if(APPLE) diff --git a/conanfile.py b/conanfile.py index 2440344..34ed9a4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -17,7 +17,7 @@ def build_requirements(self): self.tool_requires("cmake/[>=3.25.0 <3.30.0]") self.tool_requires("ninja/[>=1.12.0]") if self.options.build_testing: - # self.test_requires("catch2/3.8.0") + self.test_requires("catch2/3.8.0") self.test_requires("gtest/1.14.0") def requirements(self): diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e28b43c..fc89d32 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,4 +44,21 @@ target_compile_options(xvc_updater_tests # PRIVATE # $<$:/W4> # $<$>:-Wall> -# ) \ No newline at end of file +# ) + +add_executable(test_ws_client) + +target_sources(test_ws_client + PRIVATE + test_ws_client.cc +) +target_link_libraries(test_ws_client + PRIVATE + libxvc +) +target_compile_features(test_ws_client PRIVATE cxx_std_20) +target_compile_options(test_ws_client + PRIVATE + $<$:/W4> + $<$>:-Wall> +) \ No newline at end of file diff --git a/test/test_ws_client.cc b/test/test_ws_client.cc new file mode 100644 index 0000000..9483f69 --- /dev/null +++ b/test/test_ws_client.cc @@ -0,0 +1,28 @@ +#include + +#include + +#include "camera.h" +#include "ws_client.h" + +using json = nlohmann::json; + +int main(int argc, char **argv) +{ + auto client = xvc::ws_client("192.168.177.100", "8000", [](std::string_view event) { + auto const device_event = json::parse(event); + auto const event_type = device_event["event_type"]; + auto const camera_json = device_event["camera"]; + spdlog::info("event_type = {}", event_type.get()); + + if (event_type == "Added") { + auto cameras = Camera::parse(camera_json); + spdlog::info("Added camera name = {}", cameras->name()); + } else if (event_type == "Removed") { + auto const id = camera_json["id"].get(); + spdlog::info("Removed camera id = {}", id); + } + }); + + std::this_thread::sleep_for(std::chrono::seconds(60)); +} \ No newline at end of file diff --git a/xdaqvc/ws_client.cc b/xdaqvc/ws_client.cc index ef8414c..bbf17d8 100644 --- a/xdaqvc/ws_client.cc +++ b/xdaqvc/ws_client.cc @@ -2,55 +2,37 @@ #include -#include -#include -#include - - namespace http = beast::http; // from - -namespace -{ -auto constexpr RESOLVE = "resolve"; -auto constexpr CONNECT = "connect"; -auto constexpr HANDSHAKE = "handshake"; -auto constexpr READ = "read"; -auto constexpr CLOSE = "close"; -auto constexpr ROUTE = "/ws"; - // Report a failure -void fail(beast::error_code ec, char const *what) { spdlog::debug("{} : {}", what, ec.message()); } - -} // namespace - -namespace xvc +void fail(beast::error_code ec, std::string_view what) { + spdlog::debug("{} : {}", what, ec.message()); +} -session::session(net::io_context &ioc, std::function handler) +session::session( + std::string_view host, std::string_view port, net::io_context &ioc, + std::function event_handler +) : _resolver(net::make_strand(ioc)), _ws(net::make_strand(ioc)), - _event_handler(std::move(handler)) + _host(host), + _port(port), + _handler(std::move(event_handler)) { } -void session::request_stop() { _stopping.store(true, std::memory_order_relaxed); } - -void session::run(char const *host, char const *port) +void session::run() { - if (_stopping.load(std::memory_order_relaxed)) return; - _host = host; - // Look up the domain name _resolver.async_resolve( - host, port, beast::bind_front_handler(&session::on_resolve, shared_from_this()) + _host, _port, beast::bind_front_handler(&session::on_resolve, shared_from_this()) ); } void session::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { - if (_stopping.load(std::memory_order_relaxed)) return; - if (ec) return fail(ec, RESOLVE); + if (ec) return fail(ec, "resolve"); // Set the timeout for the operation beast::get_lowest_layer(_ws).expires_after(std::chrono::seconds(1)); @@ -63,9 +45,8 @@ void session::on_resolve(beast::error_code ec, tcp::resolver::results_type resul void session::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) { - if (_stopping.load(std::memory_order_relaxed)) return; if (ec) { - fail(ec, CONNECT); + fail(ec, "connect"); reconnect(); return; }; @@ -85,49 +66,44 @@ void session::on_connect(beast::error_code ec, tcp::resolver::results_type::endp ); })); - // Update the host_ string. This will provide the value of the + // Update the _host string. This will provide the value of the // Host HTTP header during the WebSocket handshake. // See https://tools.ietf.org/html/rfc7230#section-5.4 - _host += ':' + std::to_string(ep.port()); + _host = fmt::format("{}:{}", _host, ep.port()); // Perform the websocket handshake _ws.async_handshake( - _host, ROUTE, beast::bind_front_handler(&session::on_handshake, shared_from_this()) + _host, "/ws", beast::bind_front_handler(&session::on_handshake, shared_from_this()) ); } void session::on_handshake(beast::error_code ec) { - if (_stopping.load(std::memory_order_relaxed)) return; - if (ec) return fail(ec, HANDSHAKE); + if (ec) return fail(ec, "handshake"); read(); } void session::read() { - if (_stopping.load(std::memory_order_relaxed)) return; _ws.async_read(_buffer, beast::bind_front_handler(&session::on_read, shared_from_this())); } void session::on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - if (_stopping.load(std::memory_order_relaxed)) return; if (ec) { - fail(ec, READ); + fail(ec, "read"); reconnect(); return; }; // Process the received message - auto const event = beast::buffers_to_string(_buffer.data()); - _event_handler(event); + _handler(beast::buffers_to_string(_buffer.data())); // Clear the buffer - _buffer.clear(); - // _buffer.consume(_buffer.size()); + _buffer.consume(_buffer.size()); read(); } @@ -135,33 +111,22 @@ void session::on_read(beast::error_code ec, std::size_t bytes_transferred) void session::close() { // Close the WebSocket connection - // _ws.async_close( - // websocket::close_code::normal, - // beast::bind_front_handler(&session::on_close, shared_from_this()) - // ); - net::dispatch(_ws.get_executor(), [self = shared_from_this()] { - if (self->_stopping.load(std::memory_order_relaxed)) return; - self->_stopping.store(true, std::memory_order_relaxed); - if (self->_ws.is_open()) { - self->_ws.async_close( - websocket::close_code::normal, beast::bind_front_handler(&session::on_close, self) - ); - } - }); + _ws.async_close( + websocket::close_code::normal, + beast::bind_front_handler(&session::on_close, shared_from_this()) + ); } void session::on_close(beast::error_code ec) { - if (ec) return fail(ec, CLOSE); + if (ec) return fail(ec, "close"); // If we get here then the connection is closed gracefully - spdlog::debug("WebSocket closed gracefully"); } void session::reconnect(const std::chrono::milliseconds timeout) { - if (_stopping.load(std::memory_order_relaxed)) return; spdlog::debug("session has been disconnected, trying to reconnect..."); if (_ws.is_open()) { @@ -170,60 +135,37 @@ void session::reconnect(const std::chrono::milliseconds timeout) spdlog::debug("next trial will start after {}ms", timeout.count()); std::this_thread::sleep_for(timeout); - // auto timer = std::make_shared(_ws.get_executor()); - // timer->expires_after(timeout); - // timer->async_wait([self = shared_from_this(), timer](beast::error_code ec) { - // if (ec) return; // Timer was cancelled - - // self->run("192.168.177.100", "8000"); - // }); - auto const host = "192.168.177.100"; - auto const port = "8000"; - - run(host, port); + run(); } -ws_client::ws_client(std::function handler) : _event_handler(std::move(handler)) +namespace xvc { - _ioc = std::make_unique(); - - _thread = std::jthread([this, host = "192.168.177.100", port = "8000"]() { - try { - // Launch the asynchronous operation - _session = std::make_shared(*_ioc, [this](const std::string &event) { - _event_handler(event); - }); - _session->run(host, port); +ws_client::ws_client( + std::string_view host, std::string_view port, + std::function event_handler +) +{ + // Launch the asynchronous operation + auto _session = std::make_shared( + host, port, _ioc, [handler = std::move(event_handler)](std::string_view event) { + handler(event); + } + ); + _session->run(); + _thread = std::jthread([&]() { + try { // Run the I/O service. The call will return when // the socket is closed. - _ioc->run(); - - spdlog::info("WebSocket closed"); + _ioc.run(); } catch (const std::exception &e) { spdlog::error("WebSocket thread error: {}", e.what()); } }); - - // net::signal_set signals(*_ioc, SIGINT, SIGTERM); - // signals.async_wait([this](const boost::system::error_code &, int) { _ioc->stop(); }); } -ws_client::~ws_client() { shutdown(); } - -void ws_client::shutdown() -{ - spdlog::info("ws_client::shutdown"); - - if (_session) { - _session->request_stop(); - _session->close(); - } - if (_ioc) { - _ioc->stop(); - } -} +ws_client::~ws_client() { _ioc.stop(); } } // namespace xvc \ No newline at end of file diff --git a/xdaqvc/ws_client.h b/xdaqvc/ws_client.h index 306ab46..3fae0f4 100644 --- a/xdaqvc/ws_client.h +++ b/xdaqvc/ws_client.h @@ -11,20 +11,19 @@ #include #include #include +#include +#include #include +#include +#include #include - namespace beast = boost::beast; // from namespace websocket = beast::websocket; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from using namespace std::chrono_literals; - -namespace xvc -{ - // Sends a WebSocket message and prints the response class session : public std::enable_shared_from_this { @@ -32,25 +31,23 @@ class session : public std::enable_shared_from_this websocket::stream _ws; beast::flat_buffer _buffer; std::string _host; - std::function _event_handler; - - std::atomic _stopping{false}; + std::string _port; + std::function _handler; public: // Resolver and socket require an io_context - explicit session(net::io_context &ioc, std::function handler); - ~session() = default; + explicit session( + std::string_view host, std::string_view port, net::io_context &ioc, + std::function event_handler + ); // Start the asynchronous operation - void run(char const *host, char const *port); + void run(); void on_resolve(beast::error_code ec, tcp::resolver::results_type results); void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep); void on_handshake(beast::error_code ec); - // tell the session to stop gracefully (no more reconnects) - void request_stop(); - void read(); void on_read(beast::error_code ec, std::size_t bytes_transferred); void close(); @@ -59,19 +56,21 @@ class session : public std::enable_shared_from_this void reconnect(const std::chrono::milliseconds timeout = 500ms); }; +namespace xvc +{ + class ws_client { public: - ws_client(std::function handler = nullptr); + ws_client( + std::string_view host = "192.168.177.100", std::string_view port = "8000", + std::function event_handler = nullptr + ); ~ws_client(); - void shutdown(); - private: - std::shared_ptr _session; - std::unique_ptr _ioc; + net::io_context _ioc; std::jthread _thread; - std::function _event_handler; }; } // namespace xvc \ No newline at end of file From 26e484a9fdc4bfbcd5ee35d565fc7256c468bf31 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 16 Oct 2025 12:02:09 +0800 Subject: [PATCH 03/36] loop record default to false --- xdaqvc/xvc.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index a9d7636..fc769ff 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -28,7 +28,7 @@ void stop_h265_recording(GstPipeline *pipeline); void start_jpeg_recording( GstPipeline *pipeline, fs::path &filepath, bool continuous = true, int max_size_time = 10, - TimeUnit unit = TimeUnit::Minutes, bool loop = true, int max_files = 10 + TimeUnit unit = TimeUnit::Minutes, bool loop = false, int max_files = 10 ); void stop_jpeg_recording(GstPipeline *pipeline); From 627fe85230e19639f7109afadc83779dd902ae56 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 16 Oct 2025 12:04:16 +0800 Subject: [PATCH 04/36] Version 0.1.1 fix: ownership of io context in websocket client set default loop record option to false --- CMakeLists.txt | 2 +- conanfile.py | 2 +- xdaqvc/xvc.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b92fcd..a56604d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -set(libxvc_VERSION 0.1.0) +set(libxvc_VERSION 0.1.1) set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum OS X deployment version" FORCE) diff --git a/conanfile.py b/conanfile.py index 34ed9a4..2be1a40 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,7 +4,7 @@ class libxvc(ConanFile): name = "libxvc" - version = "0.1.0" + version = "0.1.1" settings = "os", "compiler", "build_type", "arch" generators = "VirtualRunEnv" license = "LGPL-3.0-or-later" diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index fc769ff..f7d660d 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -1,6 +1,6 @@ #pragma once -#define LIBXVC_API_VER "0.1.0" +#define LIBXVC_API_VER "0.1.1" #include From 4e75d24d4c5f5e574eb5c7f255e9193146fc80b1 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Tue, 28 Oct 2025 15:06:47 +0800 Subject: [PATCH 05/36] ci: use self-built gstreamer --- .github/conan_profiles/macos-15-arm64 | 10 ++++ .github/workflows/build.yml | 69 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 .github/conan_profiles/macos-15-arm64 create mode 100644 .github/workflows/build.yml diff --git a/.github/conan_profiles/macos-15-arm64 b/.github/conan_profiles/macos-15-arm64 new file mode 100644 index 0000000..b750648 --- /dev/null +++ b/.github/conan_profiles/macos-15-arm64 @@ -0,0 +1,10 @@ +[settings] +arch=armv8 +compiler=clang +compiler.cppstd=gnu20 +compiler.libcxx=libc++ +compiler.version=18 +os=Macos +os.version=15.0 +[conf] +tools.cmake.cmaketoolchain:generator=Ninja \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d9dcb98 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +name: build + +on: + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + os_arch: [ + { os: macos-15, arch: arm64 }, + # { os: macos-15, arch: x64 }, + ] + + runs-on: ${{ matrix.os_arch.os }} + + env: + OS_ARCH: ${{ matrix.os_arch.os }}-${{ matrix.os_arch.arch }} + + steps: + - uses: actions/checkout@v5 + + - name: Conan setup + run: conan config install .github/conan_profiles/${{ env.OS_ARCH }} -tf profiles + + - name: Install gstreamer + env: + space_name: ${{ secrets.SPACE_NAME }} + space_region: ${{ secrets.SPACE_REGION }} + run: | + curl -o gstreamer-1.26.tar.gz \ + https://${space_name}.${space_region}.digitaloceanspaces.com/gstreamer/gstreamer-1.26.tar.gz + mkdir -p ${{ runner.temp }}/gstreamer + tar -xzvf gstreamer-1.26.tar.gz -C ${{ runner.temp }}/gstreamer + + - name: Configure SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata + + - name: Install xdaqmetadata + working-directory: xdaqmetadata + env: + PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig + run: | + git clone git@github.com:kontex-neuro/xdaqmetadata.git + conan install . -b missing -pr:a ${{ env.OS_ARCH }} -s build_type=Release + cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release + cmake --build build/Release + conan export-pkg . -pr:a ${{ env.OS_ARCH }} -s build_type=Release + + - name: Install build tools + run: pip install cmake conan ninja + + - name: Cache conan + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: ${{ env.OS_ARCH }}-conan + restore-keys: | + ${{ env.OS_ARCH }}-conan + + - name: Build + env: + PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig + run: | + conan install . -b missing -pr:a ${{ env.OS_ARCH }} -s build_type=Release + cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release + cmake --build build/Release From bc131046a899abf8c488cddb297ebfc7b61aa30b Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Tue, 28 Oct 2025 15:15:37 +0800 Subject: [PATCH 06/36] ci: install build tools first --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9dcb98..1d33775 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,9 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Install build tools + run: pip install cmake conan ninja + - name: Conan setup run: conan config install .github/conan_profiles/${{ env.OS_ARCH }} -tf profiles @@ -49,9 +52,6 @@ jobs: cmake --build build/Release conan export-pkg . -pr:a ${{ env.OS_ARCH }} -s build_type=Release - - name: Install build tools - run: pip install cmake conan ninja - - name: Cache conan uses: actions/cache@v4 with: From 069ff1902092c78f8858b56fab09ef19771a13cc Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Tue, 28 Oct 2025 15:43:05 +0800 Subject: [PATCH 07/36] ci: split clone and install of xdaqmetadata --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d33775..4f8d4aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: space_region: ${{ secrets.SPACE_REGION }} run: | curl -o gstreamer-1.26.tar.gz \ - https://${space_name}.${space_region}.digitaloceanspaces.com/gstreamer/gstreamer-1.26.tar.gz + https://$space_name.$space_region.digitaloceanspaces.com/gstreamer/gstreamer-1.26.tar.gz mkdir -p ${{ runner.temp }}/gstreamer tar -xzvf gstreamer-1.26.tar.gz -C ${{ runner.temp }}/gstreamer @@ -41,12 +41,14 @@ jobs: with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata + - name: Clone xdaqmetadata + run: git clone git@github.com:kontex-neuro/xdaqmetadata.git + - name: Install xdaqmetadata working-directory: xdaqmetadata env: PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig run: | - git clone git@github.com:kontex-neuro/xdaqmetadata.git conan install . -b missing -pr:a ${{ env.OS_ARCH }} -s build_type=Release cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release cmake --build build/Release From 517c9bd68bfa90cb73e05b867f51c59876ae1ff4 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 6 Nov 2025 02:46:43 +0800 Subject: [PATCH 08/36] ci: add windows build --- .github/conan_profiles/windows-2022-x64 | 9 ++++ .github/workflows/build.yml | 66 +++++++++++++++---------- 2 files changed, 49 insertions(+), 26 deletions(-) create mode 100644 .github/conan_profiles/windows-2022-x64 diff --git a/.github/conan_profiles/windows-2022-x64 b/.github/conan_profiles/windows-2022-x64 new file mode 100644 index 0000000..5c3887e --- /dev/null +++ b/.github/conan_profiles/windows-2022-x64 @@ -0,0 +1,9 @@ +[settings] +arch=x86_64 +compiler=msvc +compiler.cppstd=20 +compiler.runtime=dynamic +compiler.version=194 +os=Windows +[conf] +tools.cmake.cmaketoolchain:generator=Ninja \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f8d4aa..1c9af6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,34 +7,56 @@ jobs: build: strategy: matrix: - os_arch: [ - { os: macos-15, arch: arm64 }, - # { os: macos-15, arch: x64 }, - ] + include: + - os: windows-2022 + arch: x64 + base_name: windows-x86_64 + - os: macos-15 + arch: arm64 + base_name: macos-arm64 - runs-on: ${{ matrix.os_arch.os }} + runs-on: ${{ matrix.os }} env: - OS_ARCH: ${{ matrix.os_arch.os }}-${{ matrix.os_arch.arch }} + gst_version: 1.26 steps: - uses: actions/checkout@v5 - name: Install build tools - run: pip install cmake conan ninja + run: pipx install cmake conan ninja - - name: Conan setup - run: conan config install .github/conan_profiles/${{ env.OS_ARCH }} -tf profiles + - name: Install conan profile + run: conan config install .github/conan_profiles/${{ matrix.os }}-${{ matrix.arch }} -tf profiles - - name: Install gstreamer + - name: Cache conan + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: ${{ matrix.base_name }}-conan-${{ hashFiles('conanfile.py') }} + restore-keys: ${{ matrix.base_name }}-conan- + + - name: Cache gstreamer + id: cache-gstreamer + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/gstreamer + key: ${{ matrix.base_name }}-gstreamer-${{ env.gst_version }} + restore-keys: ${{ matrix.base_name }}-gstreamer + + - name: Download gstreamer + if: steps.cache-gstreamer.outputs.cache-hit != 'true' env: space_name: ${{ secrets.SPACE_NAME }} space_region: ${{ secrets.SPACE_REGION }} + shell: bash run: | - curl -o gstreamer-1.26.tar.gz \ - https://$space_name.$space_region.digitaloceanspaces.com/gstreamer/gstreamer-1.26.tar.gz - mkdir -p ${{ runner.temp }}/gstreamer - tar -xzvf gstreamer-1.26.tar.gz -C ${{ runner.temp }}/gstreamer + gst_archive=${{ matrix.base_name }}-${{ env.gst_version }}.zip + + curl -o "$gst_archive" \ + "https://${space_name}.${space_region}.digitaloceanspaces.com/gstreamer/$gst_archive" + + 7z x "$gst_archive" -o"${{ runner.temp }}" -y - name: Configure SSH agent uses: webfactory/ssh-agent@v0.9.0 @@ -42,30 +64,22 @@ jobs: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata - name: Clone xdaqmetadata - run: git clone git@github.com:kontex-neuro/xdaqmetadata.git + run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git - name: Install xdaqmetadata working-directory: xdaqmetadata env: PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig run: | - conan install . -b missing -pr:a ${{ env.OS_ARCH }} -s build_type=Release + conan install . -b missing -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release cmake --build build/Release - conan export-pkg . -pr:a ${{ env.OS_ARCH }} -s build_type=Release - - - name: Cache conan - uses: actions/cache@v4 - with: - path: ~/.conan2 - key: ${{ env.OS_ARCH }}-conan - restore-keys: | - ${{ env.OS_ARCH }}-conan + conan export-pkg . -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release - name: Build env: PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig run: | - conan install . -b missing -pr:a ${{ env.OS_ARCH }} -s build_type=Release + conan install . -b missing -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release cmake --build build/Release From 9181923c2eb3b28cd9d880e3d41a1ac649e7052b Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 6 Nov 2025 02:53:55 +0800 Subject: [PATCH 09/36] ci: clone xdaqmetadata with bash shell --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c9af6e..59fbb32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,6 +64,7 @@ jobs: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata - name: Clone xdaqmetadata + shell: bash run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git - name: Install xdaqmetadata From e5a3f952b0485ed3bf9778d9edb26de84a6104e9 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 6 Nov 2025 02:58:54 +0800 Subject: [PATCH 10/36] Configure known hosts for GitHub on Windows Add step to configure GitHub as known host for Windows. --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59fbb32..efd56ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,8 +63,11 @@ jobs: with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata + - name: Add GitHub to known hosts (Windows) + if: matrix.os == 'windows-2022' + run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts + - name: Clone xdaqmetadata - shell: bash run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git - name: Install xdaqmetadata From c07cbc2901ec78acead085443ea6327d9adba571 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 6 Nov 2025 03:04:36 +0800 Subject: [PATCH 11/36] ci: use bash to ssh-keyscan and clone repo --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index efd56ee..421af33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,9 +65,11 @@ jobs: - name: Add GitHub to known hosts (Windows) if: matrix.os == 'windows-2022' + shell: bash run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts - name: Clone xdaqmetadata + shell: bash run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git - name: Install xdaqmetadata From f72ace991917f3d574572192c10099c4c3348941 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 6 Nov 2025 11:15:34 +0800 Subject: [PATCH 12/36] ci: setup compiler --- .github/workflows/build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 421af33..d9fd74c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,16 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Setup Compiler (Windows) + if: matrix.os == 'windows-2022' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Setup Compiler (macOS) + if: matrix.os == 'macos-15' + run: | + echo "CC=$(brew --prefix llvm@18)/bin/clang" >> $GITHUB_ENV + echo "CXX=$(brew --prefix llvm@18)/bin/clang++" >> $GITHUB_ENV + - name: Install build tools run: pipx install cmake conan ninja From 64f703bca1a1b2fa817b451f2ad70e9cd190b77c Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 6 Nov 2025 11:44:02 +0800 Subject: [PATCH 13/36] ci: install pkg-config on Windows --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9fd74c..176ac4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,10 @@ jobs: - name: Install build tools run: pipx install cmake conan ninja + - name: Install pkg-config (Windows) + if: matrix.os == 'windows-2022' + run: choco install pkgconfiglite + - name: Install conan profile run: conan config install .github/conan_profiles/${{ matrix.os }}-${{ matrix.arch }} -tf profiles From 6ca77b2ceab61797c3ba0a06db8750414bbd7556 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 3 Dec 2025 11:08:45 +0800 Subject: [PATCH 14/36] Upgrade dep xdaqmetadata to 0.1.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 2be1a40..aabe2eb 100644 --- a/conanfile.py +++ b/conanfile.py @@ -26,7 +26,7 @@ def requirements(self): self.requires("spdlog/1.13.0") self.requires("nlohmann_json/3.11.3") self.requires("cpr/1.10.5") - self.requires("xdaqmetadata/0.1.0") + self.requires("xdaqmetadata/0.1.1") self.requires("openssl/3.4.1") self.requires("cli11/2.5.0") From ff5199e920024d13f2396e71880566556384e851 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 3 Dec 2025 11:13:42 +0800 Subject: [PATCH 15/36] ci: install xdaqmetadata via conan index --- .../{macos-15-arm64 => macos-15-armv8} | 0 .github/workflows/build.yml | 70 ++++++++++++------- 2 files changed, 46 insertions(+), 24 deletions(-) rename .github/conan_profiles/{macos-15-arm64 => macos-15-armv8} (100%) diff --git a/.github/conan_profiles/macos-15-arm64 b/.github/conan_profiles/macos-15-armv8 similarity index 100% rename from .github/conan_profiles/macos-15-arm64 rename to .github/conan_profiles/macos-15-armv8 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 176ac4d..b6c6e27 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,13 +12,13 @@ jobs: arch: x64 base_name: windows-x86_64 - os: macos-15 - arch: arm64 - base_name: macos-arm64 + arch: armv8 + base_name: macos-armv8 runs-on: ${{ matrix.os }} env: - gst_version: 1.26 + gst_version: 1.26.8 steps: - uses: actions/checkout@v5 @@ -72,34 +72,56 @@ jobs: 7z x "$gst_archive" -o"${{ runner.temp }}" -y - - name: Configure SSH agent - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata + # - name: Configure SSH agent + # uses: webfactory/ssh-agent@v0.9.0 + # with: + # ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata + + # - name: Add GitHub to known hosts (Windows) + # if: matrix.os == 'windows-2022' + # shell: bash + # run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts + + # - name: Clone xdaqmetadata + # shell: bash + # run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git + + - name: Clone conan index + run: git clone --depth 1 --branch master https://github.com/kontex-neuro/kontex-conan.git - - name: Add GitHub to known hosts (Windows) + - name: Install xdaqmetadata (Windows) if: matrix.os == 'windows-2022' - shell: bash - run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts + working-directory: kontex-conan + run: conan create recipes/xdaqmetadata/0.1.1 -s os="Windows" -s arch="${{ matrix.arch }}" -s build_type="Release" - - name: Clone xdaqmetadata - shell: bash - run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git + - name: Install xdaqmetadata (macOS) + if: matrix.os == 'macos-15' + working-directory: kontex-conan + run: conan create recipes/xdaqmetadata/0.1.1 -s os="Macos" -s arch="${{ matrix.arch }}" -s build_type="Release" - - name: Install xdaqmetadata - working-directory: xdaqmetadata + - name: Build & Install env: PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig run: | conan install . -b missing -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release - cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release - cmake --build build/Release - conan export-pkg . -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release + cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Release + cmake --build build/Release --target install - - name: Build - env: - PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig + - name: Get & Set version + shell: bash run: | - conan install . -b missing -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release - cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release - cmake --build build/Release + version=$(conan inspect -f json . | jq -r '.version') + echo $version + echo "version=$version" >> $GITHUB_ENV + + - name: Zip + shell: bash + run: | + mv build/install ${{ matrix.base_name }} + 7z a ${{ matrix.base_name }}-${{ env.version }}.zip ${{ matrix.base_name }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.base_name }} + path: ${{ matrix.base_name }}-${{ env.version }}.zip \ No newline at end of file From 63ceed4a052b4a3087ee3da3639cf819ab9a1d5a Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 3 Dec 2025 11:28:24 +0800 Subject: [PATCH 16/36] ci: fix install xdaqmetadata with no conan profile found --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6c6e27..956e50c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,12 +92,12 @@ jobs: - name: Install xdaqmetadata (Windows) if: matrix.os == 'windows-2022' working-directory: kontex-conan - run: conan create recipes/xdaqmetadata/0.1.1 -s os="Windows" -s arch="${{ matrix.arch }}" -s build_type="Release" + run: conan create recipes/xdaqmetadata/0.1.1 -s os="Windows" -s arch="${{ matrix.arch }}" -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type="Release" - name: Install xdaqmetadata (macOS) if: matrix.os == 'macos-15' working-directory: kontex-conan - run: conan create recipes/xdaqmetadata/0.1.1 -s os="Macos" -s arch="${{ matrix.arch }}" -s build_type="Release" + run: conan create recipes/xdaqmetadata/0.1.1 -s os="Macos" -s arch="${{ matrix.arch }}" -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type="Release" - name: Build & Install env: From cf5daafe4ef8b223cf951c7bedf78428677f5e24 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 3 Dec 2025 11:39:42 +0800 Subject: [PATCH 17/36] ci: update install conan index --- .github/workflows/build.yml | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 956e50c..ff60afd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,32 +72,10 @@ jobs: 7z x "$gst_archive" -o"${{ runner.temp }}" -y - # - name: Configure SSH agent - # uses: webfactory/ssh-agent@v0.9.0 - # with: - # ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} # xdaqmetadata - - # - name: Add GitHub to known hosts (Windows) - # if: matrix.os == 'windows-2022' - # shell: bash - # run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts - - # - name: Clone xdaqmetadata - # shell: bash - # run: git clone -b dev --single-branch git@github.com:kontex-neuro/xdaqmetadata.git - - - name: Clone conan index - run: git clone --depth 1 --branch master https://github.com/kontex-neuro/kontex-conan.git - - - name: Install xdaqmetadata (Windows) - if: matrix.os == 'windows-2022' - working-directory: kontex-conan - run: conan create recipes/xdaqmetadata/0.1.1 -s os="Windows" -s arch="${{ matrix.arch }}" -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type="Release" - - - name: Install xdaqmetadata (macOS) - if: matrix.os == 'macos-15' - working-directory: kontex-conan - run: conan create recipes/xdaqmetadata/0.1.1 -s os="Macos" -s arch="${{ matrix.arch }}" -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type="Release" + - name: Setup conan index + run: | + git clone --depth 1 --branch master https://github.com/kontex-neuro/kontex-conan.git kontex-conan + conan remote add --force kontex-neuro ./kontex-conan - name: Build & Install env: From 8f396bf48d0f70ca94c61d2c2a6fbaf270eb415a Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Tue, 9 Dec 2025 17:05:13 +0800 Subject: [PATCH 18/36] Update build.yml --- .github/workflows/build.yml | 60 ++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff60afd..b39a88d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,39 +51,71 @@ jobs: restore-keys: ${{ matrix.base_name }}-conan- - name: Cache gstreamer - id: cache-gstreamer uses: actions/cache@v4 + id: cache-gstreamer with: - path: ${{ runner.temp }}/gstreamer + path: | + C:\Program Files\gstreamer + /Library/Frameworks/GStreamer.framework key: ${{ matrix.base_name }}-gstreamer-${{ env.gst_version }} - restore-keys: ${{ matrix.base_name }}-gstreamer + restore-keys: ${{ matrix.base_name }}-gstreamer- - - name: Download gstreamer - if: steps.cache-gstreamer.outputs.cache-hit != 'true' + - name: Install gstreamer (Windows) + if: matrix.os == 'windows-2022' && steps.cache-gstreamer.outputs.cache-hit != 'true' env: - space_name: ${{ secrets.SPACE_NAME }} - space_region: ${{ secrets.SPACE_REGION }} + gst_archive: gstreamer-1.0-msvc-x86_64-${{ env.gst_version }}.msi + gst_devel_archive: gstreamer-1.0-devel-msvc-x86_64-${{ env.gst_version }}.msi + run: | + curl -o "${{ env.gst_archive }}" "https://gstreamer.freedesktop.org/data/pkg/windows/${{ env.gst_version }}/msvc/${{ env.gst_archive }}" + curl -o "${{ env.gst_devel_archive }}" "https://gstreamer.freedesktop.org/data/pkg/windows/${{ env.gst_version }}/msvc/${{ env.gst_devel_archive }}" + + $log = "install.log" + + $process = Start-Process "msiexec" "/i `"${{ env.gst_archive }}`" /qn /l*! `"$log`"" -NoNewWindow -PassThru + $process_log = Start-Process "powershell" "Get-Content -Path `"$log`" -Wait" -NoNewWindow -PassThru + $process.WaitForExit() + $process_log.Kill() + + $process = Start-Process "msiexec" "/i `"${{ env.gst_devel_archive }}`" /qn /l*! `"$log`"" -NoNewWindow -PassThru + $process_log = Start-Process "powershell" "Get-Content -Path `"$log`" -Wait" -NoNewWindow -PassThru + $process.WaitForExit() + $process_log.Kill() + + - name: Install gstreamer (macOS) + if: matrix.os == 'macos-15' && steps.cache-gstreamer.outputs.cache-hit != 'true' shell: bash run: | - gst_archive=${{ matrix.base_name }}-${{ env.gst_version }}.zip + gst_archive=gstreamer-1.0-${{ env.gst_version }}-universal.pkg + gst_devel_archive=gstreamer-1.0-devel-${{ env.gst_version }}-universal.pkg - curl -o "$gst_archive" \ - "https://${space_name}.${space_region}.digitaloceanspaces.com/gstreamer/$gst_archive" + curl -o "$gst_archive" "https://gstreamer.freedesktop.org/data/pkg/osx/${{ env.gst_version }}/$gst_archive" + curl -o "$gst_devel_archive" "https://gstreamer.freedesktop.org/data/pkg/osx/${{ env.gst_version }}/$gst_devel_archive" - 7z x "$gst_archive" -o"${{ runner.temp }}" -y + sudo installer -pkg $gst_archive -target / + sudo installer -pkg $gst_devel_archive -target / - name: Setup conan index run: | git clone --depth 1 --branch master https://github.com/kontex-neuro/kontex-conan.git kontex-conan conan remote add --force kontex-neuro ./kontex-conan - - name: Build & Install + - name: Build & Install (Windows) + if: matrix.os == 'windows-2022' + env: + PKG_CONFIG_PATH: "C:\\Program Files\\gstreamer\\1.0\\msvc_x86_64\\lib\\pkgconfig" + run: | + conan install . -b missing -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release + cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Release + cmake --build build/Release --target install + + - name: Build & Install (macOS) + if: matrix.os == 'macos-15' env: - PKG_CONFIG_PATH: ${{ runner.temp }}/gstreamer/lib/pkgconfig + PKG_CONFIG_PATH: /Library/Frameworks/GStreamer.framework/Versions/1.0/lib/pkgconfig run: | conan install . -b missing -pr:a ${{ matrix.os }}-${{ matrix.arch }} -s build_type=Release cmake -S . -B build/Release --preset conan-release -G "Ninja" -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Release - cmake --build build/Release --target install + cmake --build build/Release --target install - name: Get & Set version shell: bash From e33e1a63a52b7534eb29d99aedd8f988765440c8 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 10 Dec 2025 16:35:41 +0800 Subject: [PATCH 19/36] Remove video/x-raw parsing jpeg: remove detach thread stop recording --- xdaqvc/CMakeLists.txt | 6 ++ xdaqvc/camera.cc | 173 +++++++++++++++++++++++------------------- xdaqvc/camera.h | 30 ++++---- xdaqvc/ws_client.h | 6 -- xdaqvc/xvc.cc | 95 +++++++++-------------- 5 files changed, 155 insertions(+), 155 deletions(-) diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 3e96eef..0415c53 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -44,6 +44,12 @@ target_compile_options(libxvc $<$>:-Wall> # $<$>:-Wall -Wextra -Wpedantic -Werror> ) + +if(CMAKE_BUILD_TYPE MATCHES "Debug") + target_compile_options(libxvc PUBLIC -fsanitize=address,undefined) + target_link_options(libxvc PUBLIC -fsanitize=address,undefined) +endif() + target_link_libraries(libxvc PUBLIC fmt::fmt diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index c2c60aa..768ff2b 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -3,30 +3,76 @@ #include #include -// #include +#include #include "port_pool.h" -// using nlohmann::json; - namespace { -auto constexpr Cameras = "192.168.177.100:8000/cameras"; -auto constexpr jpeg = "192.168.177.100:8000/jpeg"; -auto constexpr test = "192.168.177.100:8000/test"; -[[maybe_unused]] auto constexpr loopback = "127.0.0.1:8000/test"; -auto constexpr H265 = "192.168.177.100:8000/h265"; -auto constexpr Stop = "192.168.177.100:8000/stop"; -auto constexpr OK = 200; +constexpr auto Cameras = "http://192.168.177.100:8000/cameras"; +constexpr auto MJPEG = "http://192.168.177.100:8000/jpeg"; +constexpr auto Test = "http://192.168.177.100:8000/test"; +constexpr auto H265 = "http://192.168.177.100:8000/h265"; +constexpr auto H264 = "http://192.168.177.100:8000/h264"; +constexpr auto Stop = "http://192.168.177.100:8000/stop"; +constexpr auto OK = 200; PortPool pool(9000, 9064); +std::optional get_json(std::string_view url, std::chrono::milliseconds timeout) +{ + if (url.empty()) { + spdlog::error("GET attempted with empty URL"); + return std::nullopt; + } + + auto res = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); + if (res.status_code != OK) { + spdlog::warn( + "GET {} failed (status={}, error='{}')", url, res.status_code, res.error.message + ); + return std::nullopt; + } + + try { + return json::parse(res.text); + } catch (const std::exception &e) { + spdlog::error("JSON parse error from {}: {}", url, e.what()); + return std::nullopt; + } +}; + +cpr::Response post_json( + std::string_view url, const json &payload, const std::chrono::milliseconds timeout +) +{ + if (url.empty()) { + spdlog::error("Attempted HTTP POST with empty URL"); + return {}; + } + + return cpr::Post( + cpr::Url{url}, + cpr::Header{{"Content-Type", "application/json"}}, + cpr::Body{payload.dump(2)}, + cpr::Timeout{timeout} + ); +} + +void log(const cpr::Response &r, std::string_view action) +{ + if (r.status_code == OK) { + spdlog::info("{} succeeded", action); + } else { + spdlog::warn("{} failed. status={}, error='{}'", action, r.status_code, r.error.message); + } +} + } // namespace -Camera::Camera(const int id, const std::string &name) : _id(id), _name(name), _test(false) +Camera::Camera(const int id, std::string_view name) : _id(id), _name(name), _test(false) { - auto port = pool.allocate_port(); - if (port) { + if (auto port = pool.allocate_port()) { _port = port.value(); } spdlog::info("Creating camera id: {}, name: {}, port: {}", _id, _name, _port); @@ -38,25 +84,20 @@ Camera::~Camera() spdlog::info("Deleting camera id: {}, name: {}, port: {}", _id, _name, _port); } -// std::vector> Camera::cameras(const std::chrono::milliseconds duration) std::vector Camera::cameras(const std::chrono::milliseconds duration) { - // std::vector> cameras; std::vector cameras; - auto response = cpr::Get(cpr::Url(Cameras), cpr::Timeout(duration)); - if (response.status_code != OK) { - spdlog::warn("Failed to fetch cameras."); + auto data = get_json(Cameras, duration); + if (!data) { return cameras; } + cameras.reserve(data->size()); - try { - auto cameras_json = json::parse(response.text); - for (const auto &cam : cameras_json) { - cameras.emplace_back(Camera::parse(cam)); + for (const auto &cam_json : *data) { + if (auto cam = parse(cam_json)) { + cameras.emplace_back(cam); } - } catch (const std::exception &e) { - spdlog::error("JSON parse error: {}", e.what()); } return cameras; @@ -70,14 +111,17 @@ Camera *Camera::parse(const json &camera_json) auto const caps_json = camera_json["caps"]; auto camera = new Camera(id, name); - // auto camera = std::make_unique(id, name); + // auto camera = std::make_unique( + // camera_json["id"].get(), camera_json["name"].get() + // ); for (const auto &cap_json : caps_json) { - Camera::Cap cap; - cap.media_type = cap_json.at("media_type").get(); - cap.format = cap_json.at("format").get(); - cap.width = cap_json.at("width").get(); - cap.height = cap_json.at("height").get(); + Camera::Cap cap{ + .media_type = cap_json.at("media_type").get(), + .format = cap_json.at("format").get(), + .width = cap_json.at("width").get(), + .height = cap_json.at("height").get() + }; auto framerate_str = cap_json.at("framerate").get(); auto delimiter_pos = framerate_str.find('/'); @@ -85,15 +129,11 @@ Camera *Camera::parse(const json &camera_json) cap.fps_n = std::stoi(framerate_str.substr(0, delimiter_pos)); cap.fps_d = std::stoi(framerate_str.substr(delimiter_pos + 1)); } - camera->add_cap(cap); - // TODO: hack - if (cap.media_type == "image/jpeg") { - camera->add_codec(Codec::MJPEG); - } else if (cap.media_type == "video/x-raw") { - camera->add_codec(Codec::MJPEG); - camera->add_codec(Codec::H265); + if (cap.media_type != "image/jpeg") { + continue; } + camera->add_cap(cap); } return camera; @@ -101,52 +141,33 @@ Camera *Camera::parse(const json &camera_json) void Camera::start(const Cap &cap, const std::chrono::milliseconds duration) { - json payload; - payload["id"] = _id; - payload["capability"] = cap.to_string(); - payload["port"] = _port; + const json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; - cpr::Url url; - if (_test) { - url = cpr::Url(test); - } else if (_stream_codec == Codec::MJPEG) { - url = cpr::Url(jpeg); - } else if (_stream_codec == Codec::H265) { - url = cpr::Url(H265); - } + std::string_view url; - auto response = cpr::Post( - url, - cpr::Header{{"Content-Type", "application/json"}}, - cpr::Body(payload.dump(2)), - cpr::Timeout(duration) - ); - if (response.status_code == OK) { - spdlog::info( - "Successfully send HTTP request to start camera, id: {}, port: {}, cap: {}", - _id, - _port, - cap.to_string() - ); + if (_test) { + url = Test; + } else if (cap.media_type == "image/jpeg") { + url = MJPEG; + } else if (cap.media_type == "video/x-h265") { + url = H265; + } else if (cap.media_type == "video/x-h264") { + url = H264; } else { - spdlog::warn("Failed to start camera, status code: {}", response.status_code); + spdlog::error("Unsupported codec for camera id: {}", _id); + return; } + + auto res = post_json(url, payload, duration); + + log(res, fmt::format("Start camera id: {}", _id)); } void Camera::stop(const std::chrono::milliseconds duration) { - json payload; - payload["id"] = _id; + const json payload{{"id", _id}}; - auto response = cpr::Post( - cpr::Url(Stop), - cpr::Header{{"Content-Type", "application/json"}}, - cpr::Body(payload.dump(2)), - cpr::Timeout(duration) - ); - if (response.status_code == OK) { - spdlog::info("Successfully send HTTP request to stop camera, id: {}", _id); - } else { - spdlog::warn("Failed to stop camera, status code: {}", response.status_code); - } + auto res = post_json(Stop, payload, duration); + + log(res, fmt::format("Stop camera id: {}", _id)); } \ No newline at end of file diff --git a/xdaqvc/camera.h b/xdaqvc/camera.h index 2c33cdb..b93279a 100644 --- a/xdaqvc/camera.h +++ b/xdaqvc/camera.h @@ -43,9 +43,9 @@ class Camera } } }; - enum class Codec { MJPEG, H265, H264 }; + // enum class Codec { MJPEG, H265, H264 }; - Camera(const int id = -1, const std::string &name = ""); + Camera(const int id = -1, std::string_view name = ""); ~Camera(); // [[nodiscard]] static std::unique_ptr parse(const json &event); @@ -55,27 +55,27 @@ class Camera // const std::chrono::milliseconds duration = 500ms // ); [[nodiscard]] static std::vector cameras( - const std::chrono::milliseconds duration = 500ms + const std::chrono::milliseconds duration = 1s ); [[nodiscard]] int id() const { return _id; } [[nodiscard]] unsigned short port() const { return _port; } [[nodiscard]] std::string name() const { return _name; } - void set_name(const std::string &name) { _name = name; } + void set_name(std::string_view name) { _name = name; } [[nodiscard]] std::vector caps() const { return _caps; } void add_cap(const Cap &cap) { _caps.emplace_back(cap); } - [[nodiscard]] std::vector codecs() const { return _codecs; } - void add_codec(const Codec &codec) - { - if (std::find(_codecs.begin(), _codecs.end(), codec) == _codecs.end()) { - _codecs.emplace_back(codec); - } - } + // [[nodiscard]] std::vector codecs() const { return _codecs; } + // void add_codec(const Codec &codec) + // { + // if (std::find(_codecs.begin(), _codecs.end(), codec) == _codecs.end()) { + // _codecs.emplace_back(codec); + // } + // } - [[nodiscard]] Codec stream_codec() const { return _stream_codec; } - void set_stream_codec(const Codec &codec) { _stream_codec = codec; } + // [[nodiscard]] Codec stream_codec() const { return _stream_codec; } + // void set_stream_codec(const Codec &codec) { _stream_codec = codec; } void start(const Cap &cap, std::chrono::milliseconds duration = 500ms); void stop(const std::chrono::milliseconds duration = 500ms); @@ -89,8 +89,8 @@ class Camera std::string _name; std::vector _caps; - std::vector _codecs; - Codec _stream_codec; + // std::vector _codecs; + // Codec _stream_codec; bool _test; }; \ No newline at end of file diff --git a/xdaqvc/ws_client.h b/xdaqvc/ws_client.h index 3fae0f4..25a59a4 100644 --- a/xdaqvc/ws_client.h +++ b/xdaqvc/ws_client.h @@ -1,12 +1,6 @@ #pragma once -#ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0601 -#endif - -#ifndef _WIN32_WINDOWS -#define _WIN32_WINDOWS 0x0601 -#endif #include #include diff --git a/xdaqvc/xvc.cc b/xdaqvc/xvc.cc index 5df6a1d..91a230a 100644 --- a/xdaqvc/xvc.cc +++ b/xdaqvc/xvc.cc @@ -398,15 +398,20 @@ void start_jpeg_recording( } } - spdlog::info("Start GStreamer M-JPEG recording"); + spdlog::info("Start M-JPEG recording"); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - auto src_pad = gst_element_request_pad_simple(tee, "src_1"); + if (auto exist_tee_srcpad = gst_element_get_static_pad(tee, "src_1")) { + spdlog::warn("tee 'src_1' pad already exists, releasing it..."); + gst_element_release_request_pad(tee, exist_tee_srcpad); + gst_object_unref(exist_tee_srcpad); + } + auto tee_srcpad = gst_element_request_pad_simple(tee, "src_1"); - auto queue_record = create_element("queue", "queue_record"); + auto queue = create_element("queue", "queue_record"); auto parser = create_element("jpegparse", "record_parser"); - auto filesink = create_element("splitmuxsink", "filesink"); auto muxer = create_element("matroskamux", "muxer"); + auto filesink = create_element("splitmuxsink", "filesink"); switch (unit) { case TimeUnit::Minutes: max_size_time = max_size_time * 60; break; @@ -423,8 +428,12 @@ void start_jpeg_recording( g_signal_connect(filesink, "format-location", G_CALLBACK(generate_filename), tracker.release()); // clang-format off - g_object_set(G_OBJECT(muxer), "timecodescale", 1, nullptr); - g_object_set(G_OBJECT(muxer), "offset-to-zero", true, nullptr); + g_object_set( + G_OBJECT(muxer), + "timecodescale", 1, + "offset-to-zero", true, + nullptr + ); g_object_set( G_OBJECT(filesink), "max-size-time", split ? max_size_time * GST_SECOND : 0, // max-size-time=0 -> continuous @@ -434,28 +443,24 @@ void start_jpeg_recording( ); // clang-format on - gst_bin_add_many(GST_BIN(pipeline), queue_record, parser, filesink, nullptr); + gst_bin_add_many(GST_BIN(pipeline), queue, parser, filesink, nullptr); - if (!gst_element_link_many(queue_record, parser, filesink, nullptr)) { + if (!gst_element_link_many(queue, parser, filesink, nullptr)) { spdlog::error("Elements could not be linked."); - gst_object_unref(pipeline); return; } - gst_element_sync_state_with_parent(queue_record); + gst_element_sync_state_with_parent(queue); gst_element_sync_state_with_parent(parser); gst_element_sync_state_with_parent(filesink); - std::unique_ptr sink_pad( - gst_element_get_static_pad(queue_record, "sink"), gst_object_unref - ); - - auto ret = gst_pad_link(src_pad, sink_pad.get()); - if (GST_PAD_LINK_FAILED(ret)) { - spdlog::error("Failed to link 'tee' src pad to 'queue' sink pad"); - gst_object_unref(pipeline); - return; + auto queue_sinkpad = gst_element_get_static_pad(queue, "sink"); + if (gst_pad_link(tee_srcpad, queue_sinkpad) != GST_PAD_LINK_OK) { + spdlog::error("Failed to link 'tee' srcpad to 'queue' sinkpad"); } + gst_object_unref(queue_sinkpad); + gst_object_unref(tee); + GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "after-link"); } @@ -465,56 +470,30 @@ void stop_jpeg_recording(GstPipeline *pipeline) spdlog::error("Pipeline is null"); return; } - spdlog::info("Stop GStreamer M-JPEG recording"); + spdlog::info("Stop M-JPEG recording"); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - auto src_pad = gst_element_get_static_pad(tee, "src_1"); - - // Increase the reference count so that 'pipeline' remains valid. - gst_object_ref(pipeline); + auto tee_srcpad = gst_element_get_static_pad(tee, "src_1"); gst_pad_add_probe( - src_pad, + tee_srcpad, GST_PAD_PROBE_TYPE_IDLE, - [](GstPad *src_pad, GstPadProbeInfo *, gpointer user_data) -> GstPadProbeReturn { + [](GstPad *tee_srcpad, + [[maybe_unused]] GstPadProbeInfo *info, + gpointer user_data) -> GstPadProbeReturn { spdlog::info("Unlinking"); auto pipeline = GST_PIPELINE(user_data); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - std::unique_ptr queue_record( - gst_bin_get_by_name(GST_BIN(pipeline), "queue_record"), gst_object_unref - ); - std::unique_ptr parser( - gst_bin_get_by_name(GST_BIN(pipeline), "record_parser"), gst_object_unref - ); - std::unique_ptr filesink( - gst_bin_get_by_name(GST_BIN(pipeline), "filesink"), gst_object_unref - ); - std::unique_ptr sink_pad( - gst_element_get_static_pad(queue_record.get(), "sink"), gst_object_unref - ); - gst_pad_send_event(sink_pad.get(), gst_event_new_eos()); + auto queue = gst_bin_get_by_name(GST_BIN(pipeline), "queue_record"); + auto queue_sinkpad = gst_element_get_static_pad(queue, "sink"); - // Launch a detached thread to remove the elements after a delay. - std::thread([pipeline, // captured pipeline (ref'ed) - queue_record = std::move(queue_record), - parser = std::move(parser), - filesink = std::move(filesink)]() { - std::this_thread::sleep_for(std::chrono::milliseconds(3500)); - gst_bin_remove(GST_BIN(pipeline), queue_record.get()); - gst_bin_remove(GST_BIN(pipeline), parser.get()); - gst_bin_remove(GST_BIN(pipeline), filesink.get()); - - gst_element_set_state(queue_record.get(), GST_STATE_NULL); - gst_element_set_state(parser.get(), GST_STATE_NULL); - gst_element_set_state(filesink.get(), GST_STATE_NULL); + gst_pad_send_event(queue_sinkpad, gst_event_new_eos()); - // Release the extra reference on the pipeline. - gst_object_unref(pipeline); - }).detach(); - - gst_element_release_request_pad(tee, src_pad); - gst_object_unref(src_pad); + gst_object_unref(tee_srcpad); + gst_object_unref(tee); + gst_object_unref(queue_sinkpad); + gst_object_unref(queue); return GST_PAD_PROBE_REMOVE; }, From f9a27e7cc8bf7bb47d574d2a6967bdc2bb19e243 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 10 Dec 2025 16:36:30 +0800 Subject: [PATCH 20/36] Bump version into 0.1.2 --- CMakeLists.txt | 2 +- conanfile.py | 2 +- xdaqvc/xvc.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a56604d..4a73e3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -set(libxvc_VERSION 0.1.1) +set(libxvc_VERSION 0.1.2) set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum OS X deployment version" FORCE) diff --git a/conanfile.py b/conanfile.py index aabe2eb..e7ec9f9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,7 +4,7 @@ class libxvc(ConanFile): name = "libxvc" - version = "0.1.1" + version = "0.1.2" settings = "os", "compiler", "build_type", "arch" generators = "VirtualRunEnv" license = "LGPL-3.0-or-later" diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index f7d660d..42b6939 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -1,6 +1,6 @@ #pragma once -#define LIBXVC_API_VER "0.1.1" +#define LIBXVC_API_VER "0.1.2" #include From 00b653d28ecc075aabfb81cf81d3c5b3e82bec52 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Tue, 30 Dec 2025 12:36:31 +0800 Subject: [PATCH 21/36] add parsing device id of camera --- xdaqvc/camera.cc | 14 ++++++++++---- xdaqvc/camera.h | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index 768ff2b..9172d27 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -70,18 +70,23 @@ void log(const cpr::Response &r, std::string_view action) } // namespace -Camera::Camera(const int id, std::string_view name) : _id(id), _name(name), _test(false) +Camera::Camera(const int id, std::string_view device_id, std::string_view name) + : _id(id), _device_id(device_id), _name(name), _test(false) { if (auto port = pool.allocate_port()) { _port = port.value(); } - spdlog::info("Creating camera id: {}, name: {}, port: {}", _id, _name, _port); + spdlog::info( + "Creating camera id: {}, device_id: {}, name: {}, port: {}", _id, _device_id, _name, _port + ); } Camera::~Camera() { pool.release_port(_port); - spdlog::info("Deleting camera id: {}, name: {}, port: {}", _id, _name, _port); + spdlog::info( + "Deleting camera id: {}, device_id: {}, name: {}, port: {}", _id, _device_id, _name, _port + ); } std::vector Camera::cameras(const std::chrono::milliseconds duration) @@ -107,10 +112,11 @@ std::vector Camera::cameras(const std::chrono::milliseconds duration) Camera *Camera::parse(const json &camera_json) { auto const id = camera_json["id"].get(); + auto const device_id = camera_json["device_id"].get(); auto const name = camera_json["name"].get(); auto const caps_json = camera_json["caps"]; - auto camera = new Camera(id, name); + auto camera = new Camera(id, device_id, name); // auto camera = std::make_unique( // camera_json["id"].get(), camera_json["name"].get() // ); diff --git a/xdaqvc/camera.h b/xdaqvc/camera.h index b93279a..24bef0e 100644 --- a/xdaqvc/camera.h +++ b/xdaqvc/camera.h @@ -45,7 +45,7 @@ class Camera }; // enum class Codec { MJPEG, H265, H264 }; - Camera(const int id = -1, std::string_view name = ""); + Camera(const int id = -1, std::string_view device_id = "", std::string_view name = ""); ~Camera(); // [[nodiscard]] static std::unique_ptr parse(const json &event); @@ -58,6 +58,7 @@ class Camera const std::chrono::milliseconds duration = 1s ); [[nodiscard]] int id() const { return _id; } + [[nodiscard]] std::string device_id() const { return _device_id; } [[nodiscard]] unsigned short port() const { return _port; } [[nodiscard]] std::string name() const { return _name; } @@ -85,6 +86,7 @@ class Camera private: int _id; + std::string _device_id; unsigned short _port; std::string _name; From ad6f3f966d2b2d0ec35a2cdf1dc5d9d50f8da9c1 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Tue, 30 Dec 2025 12:38:34 +0800 Subject: [PATCH 22/36] Bump version into v0.2.0 --- CMakeLists.txt | 2 +- conanfile.py | 2 +- xdaqvc/xvc.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a73e3b..cf98ffd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -set(libxvc_VERSION 0.1.2) +set(libxvc_VERSION 0.2.0) set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum OS X deployment version" FORCE) diff --git a/conanfile.py b/conanfile.py index e7ec9f9..38b0375 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,7 +4,7 @@ class libxvc(ConanFile): name = "libxvc" - version = "0.1.2" + version = "0.2.0" settings = "os", "compiler", "build_type", "arch" generators = "VirtualRunEnv" license = "LGPL-3.0-or-later" diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index 42b6939..68f459f 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -1,6 +1,6 @@ #pragma once -#define LIBXVC_API_VER "0.1.2" +#define LIBXVC_API_VER "0.2.0" #include From e282b1d2af4c2290c71b43365bf2183948b9f50c Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Mon, 19 Jan 2026 18:06:48 +0800 Subject: [PATCH 23/36] fix: occasional crash when stopping recording --- xdaqvc/xvc.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xdaqvc/xvc.cc b/xdaqvc/xvc.cc index 91a230a..649978b 100644 --- a/xdaqvc/xvc.cc +++ b/xdaqvc/xvc.cc @@ -484,14 +484,12 @@ void stop_jpeg_recording(GstPipeline *pipeline) spdlog::info("Unlinking"); auto pipeline = GST_PIPELINE(user_data); - auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); auto queue = gst_bin_get_by_name(GST_BIN(pipeline), "queue_record"); auto queue_sinkpad = gst_element_get_static_pad(queue, "sink"); + gst_pad_unlink(tee_srcpad, queue_sinkpad); gst_pad_send_event(queue_sinkpad, gst_event_new_eos()); - gst_object_unref(tee_srcpad); - gst_object_unref(tee); gst_object_unref(queue_sinkpad); gst_object_unref(queue); From 8dc01a8fc4880038656f6670d3bb0f99988b59a6 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Mon, 19 Jan 2026 18:08:23 +0800 Subject: [PATCH 24/36] Bump version to 0.2.1 --- CMakeLists.txt | 2 +- conanfile.py | 2 +- xdaqvc/xvc.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf98ffd..1df5c07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -set(libxvc_VERSION 0.2.0) +set(libxvc_VERSION 0.2.1) set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum OS X deployment version" FORCE) diff --git a/conanfile.py b/conanfile.py index 38b0375..2c805ef 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,7 +4,7 @@ class libxvc(ConanFile): name = "libxvc" - version = "0.2.0" + version = "0.2.1" settings = "os", "compiler", "build_type", "arch" generators = "VirtualRunEnv" license = "LGPL-3.0-or-later" diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index 68f459f..3e75ecf 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -1,6 +1,6 @@ #pragma once -#define LIBXVC_API_VER "0.2.0" +#define LIBXVC_API_VER "0.2.1" #include From 4b96e1724840646a91002572753e9b96fdec227c Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 11 Feb 2026 17:33:05 +0800 Subject: [PATCH 25/36] Update start_jpeg_recording API Hide third-party library from headers Upgrade cpp standard to 23 Add tests for camera & port_pool & ws_client Add json schema parsing camera Update CLI Seperate updater and xvc depedencies --- CMakeLists.txt | 6 +- conanfile.py | 3 +- test/CMakeLists.txt | 58 ++++---- test/test_camera.cc | 101 +++++++++++++ test/test_port_pool.cc | 146 ++++++------------- test/test_ws_client.cc | 52 +++++-- tool/CMakeLists.txt | 10 +- tool/tvcli.cc | 313 +++++++++++++++++++--------------------- tool/xvc_update_tool.cc | 1 + xdaqvc/CMakeLists.txt | 68 ++++++--- xdaqvc/camera.cc | 190 ++++++++++++++---------- xdaqvc/camera.h | 65 +++------ xdaqvc/common.h | 72 +++++++++ xdaqvc/port_pool.cc | 118 ++++++++------- xdaqvc/port_pool.h | 28 +++- xdaqvc/server.cc | 145 +++++-------------- xdaqvc/server.h | 38 ++--- xdaqvc/updater.cc | 69 +-------- xdaqvc/updater.h | 29 +--- xdaqvc/validator.cc | 57 ++++++++ xdaqvc/validator.h | 6 + xdaqvc/ws_client.cc | 28 ++-- xdaqvc/ws_client.h | 16 +- xdaqvc/xvc.cc | 217 ++++++---------------------- xdaqvc/xvc.h | 33 +++-- 25 files changed, 894 insertions(+), 975 deletions(-) create mode 100644 test/test_camera.cc create mode 100644 xdaqvc/common.h create mode 100644 xdaqvc/validator.cc create mode 100644 xdaqvc/validator.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1df5c07..713c2f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,8 +2,6 @@ cmake_minimum_required(VERSION 3.25) set(libxvc_VERSION 0.2.1) -set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum OS X deployment version" FORCE) - project(libxvc LANGUAGES CXX VERSION "${libxvc_VERSION}" @@ -15,13 +13,13 @@ if(APPLE) add_compile_options($<$:-fexperimental-library>) endif() -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(Boost_USE_STATIC_LIBS ON) find_package(fmt REQUIRED) find_package(spdlog REQUIRED) find_package(nlohmann_json REQUIRED) +find_package(nlohmann_json_schema_validator REQUIRED) find_package(cpr REQUIRED) find_package(xdaqmetadata REQUIRED) find_package(Boost 1.81.0 REQUIRED) @@ -33,6 +31,8 @@ pkg_search_module(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0>=1.4) pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4) pkg_search_module(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0>=1.4) +option(BUILD_TESTING "Build tests" OFF) + add_subdirectory(xdaqvc) add_subdirectory(tool) diff --git a/conanfile.py b/conanfile.py index 2c805ef..f619d1b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -25,6 +25,7 @@ def requirements(self): self.requires("fmt/10.2.1") self.requires("spdlog/1.13.0") self.requires("nlohmann_json/3.11.3") + self.requires("json-schema-validator/2.3.0") self.requires("cpr/1.10.5") self.requires("xdaqmetadata/0.1.1") self.requires("openssl/3.4.1") @@ -95,4 +96,4 @@ def package(self): cmake.install() def package_info(self): - self.cpp_info.libs = ["libxvc"] + self.cpp_info.libs = ["xvc"] diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fc89d32..60bdde2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,4 @@ add_executable(xvc_updater_tests) - target_sources(xvc_updater_tests PRIVATE updater_test_base.h @@ -7,9 +6,8 @@ target_sources(xvc_updater_tests ) target_link_libraries(xvc_updater_tests PRIVATE - libxvc + updater gtest::gtest - openssl::openssl ) target_include_directories(xvc_updater_tests INTERFACE @@ -21,44 +19,48 @@ add_test( NAME xvc_updater_tests COMMAND xvc_updater_tests ) -target_compile_features(xvc_updater_tests PRIVATE cxx_std_20) +target_compile_features(xvc_updater_tests PRIVATE cxx_std_23) target_compile_options(xvc_updater_tests PRIVATE $<$:/W4> $<$>:-Wall> ) -# add_executable(test_port_pool) - -# target_sources(test_port_pool -# PRIVATE -# test_port_pool.cc -# ) -# target_link_libraries(test_port_pool -# PRIVATE -# Catch2::Catch2 -# libxvc -# ) -# target_compile_features(test_port_pool PRIVATE cxx_std_20) -# target_compile_options(test_port_pool -# PRIVATE -# $<$:/W4> -# $<$>:-Wall> -# ) - -add_executable(test_ws_client) - -target_sources(test_ws_client +add_executable(test_port_pool test_port_pool.cc) +target_link_libraries(test_port_pool PRIVATE - test_ws_client.cc + xvc + Catch2::Catch2WithMain +) +target_compile_features(test_port_pool PRIVATE cxx_std_23) +target_compile_options(test_port_pool + PRIVATE + $<$:/W4> + $<$>:-Wall> ) + +add_executable(test_ws_client test_ws_client.cc) target_link_libraries(test_ws_client PRIVATE - libxvc + xvc + Catch2::Catch2WithMain ) -target_compile_features(test_ws_client PRIVATE cxx_std_20) +target_compile_features(test_ws_client PRIVATE cxx_std_23) target_compile_options(test_ws_client PRIVATE $<$:/W4> $<$>:-Wall> +) + +add_executable(test_camera test_camera.cc) +target_link_libraries(test_camera + PRIVATE + xvc + Catch2::Catch2WithMain +) +target_compile_features(test_camera PRIVATE cxx_std_23) +target_compile_options(test_camera + PRIVATE + $<$:/W4> + $<$>:-Wall> ) \ No newline at end of file diff --git a/test/test_camera.cc b/test/test_camera.cc new file mode 100644 index 0000000..0ad7ab9 --- /dev/null +++ b/test/test_camera.cc @@ -0,0 +1,101 @@ +#include + +#include "camera.h" + +TEST_CASE("Camera::parse", "[camera][parse]") +{ + SECTION("Parses valid camera JSON") + { + constexpr auto json = R"( + { + "id": 1, + "device_id": "0000:XXXX", + "name": "Test Camera", + "caps": [ + { + "media_type": "video/x-h264", + "format": "H264", + "width": 1920, + "height": 1080, + "framerate": "30/1" + } + ] + } + )"; + auto camera = Camera::parse(json); + REQUIRE(camera); + } + + SECTION("Rejects non-object JSON") + { + constexpr auto json = R"( + [ + { + "id": 1, + "device_id": "0000:XXXX", + "name": "Test Camera", + "caps": [ + { + "media_type": "video/x-h264", + "format": "H264", + "width": 1920, + "height": 1080, + "framerate": "30/1" + } + ] + } + ] + )"; + auto camera = Camera::parse(json); + REQUIRE_FALSE(camera); + } + + SECTION("Rejects JSON missing required field") + { + constexpr auto json = R"( + { + "device_id": "0000:XXXX", + "name": "Test Camera", + "caps": [ + { + "media_type": "video/x-h264", + "format": "H264", + "width": 1920, + "height": 1080, + "framerate": "30/1" + } + ] + } + )"; + auto camera = Camera::parse(json); + REQUIRE_FALSE(camera); + } +} + +TEST_CASE("Camera port allocation does not collide", "[camera][portpool]") +{ + using namespace std::chrono_literals; + + SECTION("Exhausting port pool of camera throws runtime_error") + { + std::vector> cameras; + const auto size = 64; + cameras.reserve(size); + for (auto i = 0; i < size; ++i) { + auto cam = std::make_unique(0, "0000:XXXX", "cam"); + cameras.push_back(std::move(cam)); + } + REQUIRE_THROWS_AS(Camera(0, "0000:XXXX", "cam"), std::runtime_error); + } + + SECTION("Port is freed when camera is destroyed") + { + auto cam1 = std::make_unique(0, "0000:XXXX", "cam"); + auto port1 = cam1->port(); + cam1.reset(); + auto cam2 = std::make_unique(0, "0000:XXXX", "cam"); + auto port2 = cam2->port(); + + REQUIRE(port2 == port1); + } +} \ No newline at end of file diff --git a/test/test_port_pool.cc b/test/test_port_pool.cc index d1ce28c..f0c9035 100644 --- a/test/test_port_pool.cc +++ b/test/test_port_pool.cc @@ -1,117 +1,53 @@ -#include +#include +#include +#include #include "port_pool.h" -int main() +TEST_CASE("PortPool constructs with valid range", "[port_pool]") { - try { - PortPool pool1(9000, 9010); - PortPool pool2(9000, 9010); - - pool1.print_available_ports(); - pool2.print_available_ports(); - - auto port1 = pool1.allocate_port(); - auto port2 = pool2.allocate_port(); - - pool1.print_available_ports(); - pool1.release_port(port1.value()); - - pool2.print_available_ports(); - pool2.release_port(port2.value()); - - std::vector allocated1; - try { - for (auto i = 0; i < 20; ++i) { - auto port = pool1.allocate_port(); - if (!port) continue; - allocated1.push_back(port.value()); - } - } catch (const std::exception &ex) { - fmt::println("Expected exception on exhaustion: {}", ex.what()); - } - - std::vector allocated2; - try { - for (auto i = 0; i < 20; ++i) { - auto port = pool2.allocate_port(); - if (!port) continue; - allocated2.push_back(port.value()); - } - } catch (const std::exception &ex) { - fmt::println("Expected exception on exhaustion: {}", ex.what()); + SECTION("Constructs with valid range") { REQUIRE_NOTHROW(PortPool{9000, 9005}); } + + SECTION("Allocates unique ports within range") + { + PortPool pool(9000, 9005); + std::set allocated; + for (auto i = 0; i < 5; ++i) { + auto port = pool.allocate(); + REQUIRE(port.has_value()); + REQUIRE(port.value() >= 9000); + REQUIRE(port.value() < 9005); + REQUIRE(allocated.insert(port.value()).second); } - - } catch (const std::exception &ex) { - fmt::println("Test failed: {}", ex.what()); } - return 0; -} - - -// #include -// #include - -// #include "port_pool.h" - -// TEST_CASE("PortPool allocates a valid port", "[allocate]") -// { -// PortPool pool(40000, 40010); -// auto port = pool.allocate_port(); - -// REQUIRE(port.has_value()); -// REQUIRE(port.value() >= 40000); -// REQUIRE(port.value() < 40010); -// } - -// TEST_CASE("PortPool allocates all ports then fails", "[allocate][exhaust]") -// { -// const int start = 40100; -// const int end = 40105; -// PortPool pool(start, end); - -// std::set allocated_ports; -// for (int i = 0; i < (end - start); ++i) { -// auto port = pool.allocate_port(); -// REQUIRE(port.has_value()); -// allocated_ports.insert(port.value()); -// } - -// REQUIRE(allocated_ports.size() == static_cast(end - start)); - -// SECTION("No ports should be available now") -// { -// auto port = pool.allocate_port(); -// REQUIRE_FALSE(port.has_value()); -// } -// } - -// TEST_CASE("PortPool releases and reallocates a port", "[release][reuse]") -// { -// PortPool pool(40200, 40203); - -// auto port1 = pool.allocate_port(); -// REQUIRE(port1.has_value()); + SECTION("Failed to allocate when pool is exhausted") + { + PortPool pool(9000, 9002); + REQUIRE(pool.allocate()); + REQUIRE(pool.allocate()); + REQUIRE_FALSE(pool.allocate()); + } -// pool.release_port(port1.value()); + SECTION("Released ports can be reallocated") + { + PortPool pool(9000, 9002); -// auto port2 = pool.allocate_port(); -// REQUIRE(port2.has_value()); -// REQUIRE(port2.value() == port1.value()); -// } + auto p1 = pool.allocate(); + auto p2 = pool.allocate(); + REQUIRE(p1); + REQUIRE(p2); -// TEST_CASE("PortPool rejects invalid range", "[ctor]") -// { -// REQUIRE_THROWS_AS(PortPool(5000, 5000), std::invalid_argument); -// REQUIRE_THROWS_AS(PortPool(5001, 5000), std::invalid_argument); -// } + pool.release(p1.value()); + auto p3 = pool.allocate(); + REQUIRE(p3); -// TEST_CASE("PortPool handles out-of-range releases safely", "[release]") -// { -// PortPool pool(40300, 40302); + REQUIRE(p3.value() == p1.value()); + } -// REQUIRE_NOTHROW(pool.release_port(40299)); -// REQUIRE_NOTHROW(pool.release_port(40302)); // Equal to end -// REQUIRE_NOTHROW(pool.release_port(50000)); // Far outside -// } + SECTION("Releasing port which is out of range is safe") + { + PortPool pool(9000, 9002); + REQUIRE_FALSE(pool.release(9002)); + } +} diff --git a/test/test_ws_client.cc b/test/test_ws_client.cc index 9483f69..b4ba026 100644 --- a/test/test_ws_client.cc +++ b/test/test_ws_client.cc @@ -1,28 +1,54 @@ #include +#include #include #include "camera.h" #include "ws_client.h" -using json = nlohmann::json; +using namespace std::chrono_literals; -int main(int argc, char **argv) +TEST_CASE("WebSocket add/remove cameras", "[ws][camera]") { - auto client = xvc::ws_client("192.168.177.100", "8000", [](std::string_view event) { - auto const device_event = json::parse(event); - auto const event_type = device_event["event_type"]; - auto const camera_json = device_event["camera"]; - spdlog::info("event_type = {}", event_type.get()); + std::atomic received_event{false}; + std::vector> cameras; + + auto client = xvc::ws_client("192.168.177.100", "8000", [&](std::string event) { + REQUIRE_NOTHROW(nlohmann::json::parse(event)); + + auto const device_event = nlohmann::json::parse(event); + REQUIRE(device_event.contains("event_type")); + REQUIRE(device_event.contains("camera")); + + auto const &event_type = device_event.at("event_type").get(); + auto const &camera_json = device_event.at("camera"); if (event_type == "Added") { - auto cameras = Camera::parse(camera_json); - spdlog::info("Added camera name = {}", cameras->name()); + auto camera = Camera::parse(camera_json.dump()); + REQUIRE(camera); + + cameras.emplace_back(std::move(camera)); + received_event = true; + } else if (event_type == "Removed") { - auto const id = camera_json["id"].get(); - spdlog::info("Removed camera id = {}", id); + REQUIRE(camera_json.contains("id")); + + auto const id = camera_json.at("id").get(); + cameras.erase( + std::remove_if( + cameras.begin(), + cameras.end(), + [id](const std::unique_ptr &cam) { return cam->id() == id; } + ), + cameras.end() + ); + received_event = true; + + } else { + FAIL("Unknown event_type: " + event_type); } }); - std::this_thread::sleep_for(std::chrono::seconds(60)); -} \ No newline at end of file + std::this_thread::sleep_for(10s); + REQUIRE(received_event); +} diff --git a/tool/CMakeLists.txt b/tool/CMakeLists.txt index fe8e5f0..a866b14 100644 --- a/tool/CMakeLists.txt +++ b/tool/CMakeLists.txt @@ -1,27 +1,25 @@ add_executable(tvcli tvcli.cc) target_link_libraries(tvcli PRIVATE - libxvc + xvc CLI11::CLI11 PkgConfig::gstreamer-app PkgConfig::gstreamer-video ) -target_compile_features(tvcli PRIVATE cxx_std_20) +target_compile_features(tvcli PRIVATE cxx_std_23) target_compile_options(tvcli PRIVATE $<$:/W4> - # $<$:/W4 /WX> $<$>:-Wall> - # $<$>:-Wall -Wextra -Wpedantic -Werror> ) add_executable(xvc_update_tool xvc_update_tool.cc) target_link_libraries(xvc_update_tool PRIVATE - libxvc + updater CLI11::CLI11 ) -target_compile_features(xvc_update_tool PRIVATE cxx_std_20) +target_compile_features(xvc_update_tool PRIVATE cxx_std_23) target_compile_options(xvc_update_tool PRIVATE $<$:/W4> diff --git a/tool/tvcli.cc b/tool/tvcli.cc index a7ef62e..8696dcc 100644 --- a/tool/tvcli.cc +++ b/tool/tvcli.cc @@ -1,18 +1,13 @@ -#include #include #include -#include -#include -#include -#include #include -#include #include #include -#include #include +#include #include +#include #include #include "camera.h" @@ -20,16 +15,21 @@ #include "xdaqmetadata/metadata_handler.h" #include "xvc.h" +namespace fs = std::filesystem; + namespace { GMainLoop *loop = nullptr; GstElement *pipeline = nullptr; -MetadataHandler *handler = nullptr; bool record = false; -Camera *stream_cam = nullptr; -std::vector cams; +std::unique_ptr stream_cam = nullptr; +std::unique_ptr handler = nullptr; +std::chrono::steady_clock::time_point stream_duration; + +enum class Codec : int { MJPEG }; +enum class TimeUnit : int { Seconds, Minutes, Hours, Days }; GstFlowReturn draw_image(GstAppSink *sink, [[maybe_unused]] void *user_data) { @@ -39,62 +39,66 @@ GstFlowReturn draw_image(GstAppSink *sink, [[maybe_unused]] void *user_data) if (!sample) return GST_FLOW_OK; auto buffer = gst_sample_get_buffer(sample.get()); - GstMapInfo info; - if (gst_buffer_map(buffer, &info, GST_MAP_READ)) { - std::unique_ptr video_info( - gst_video_info_new(), gst_video_info_free - ); - if (!gst_video_info_from_caps(video_info.get(), gst_sample_get_caps(sample.get()))) { - spdlog::critical("Failed to parse video info"); - gst_buffer_unmap(buffer, &info); - return GST_FLOW_ERROR; - } - auto caps = gst_sample_get_caps(sample.get()); - auto structure = gst_caps_get_structure(caps, 0); - auto width = static_cast(g_value_get_int(gst_structure_get_value(structure, "width"))); - auto height = - static_cast(g_value_get_int(gst_structure_get_value(structure, "height"))); - auto buffer_pts = GST_BUFFER_PTS(buffer); - - auto xdaqmetadata = handler->safe_deque.check_pts_pop_timestamp(buffer_pts); - auto metadata = xdaqmetadata.value_or(XDAQFrameData{0, 0, 0, 0, 0, 0}); - - spdlog::info( - "Received buffer: size={}, pts={}, width={}, height={}, " - "fpga_timestamp={}, rhythm_timestamp={}, ttl_in={}, ttl_out={}, spi_perf_counter={}, " - "reserved={}", - gst_buffer_get_size(buffer), - buffer_pts, - width, - height, - metadata.fpga_timestamp, - metadata.rhythm_timestamp, - metadata.ttl_in, - metadata.ttl_out, - metadata.spi_perf_counter, - metadata.reserved - ); - gst_buffer_unmap(buffer, &info); + std::unique_ptr video_info( + gst_video_info_new(), gst_video_info_free + ); + if (!gst_video_info_from_caps(video_info.get(), gst_sample_get_caps(sample.get()))) { + std::println("Failed to parse video info"); + return GST_FLOW_ERROR; } + + const auto caps = gst_sample_get_caps(sample.get()); + const auto structure = gst_caps_get_structure(caps, 0); + const auto width = + static_cast(g_value_get_int(gst_structure_get_value(structure, "width"))); + const auto height = + static_cast(g_value_get_int(gst_structure_get_value(structure, "height"))); + const auto buffer_pts = GST_BUFFER_PTS(buffer); + + auto xdaqmetadata = handler->_safe_queue.dequeue(buffer_pts); + if (!xdaqmetadata) { + std::println("Failed to dequeue XDAQ metadata from buffer with PTS {}", buffer_pts); + return GST_FLOW_OK; + } + + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - stream_duration).count(); + + auto hours = elapsed / 3600; + auto minutes = (elapsed % 3600) / 60; + auto seconds = elapsed % 60; + + std::println( + "Received buffer: size={}, width={}, height={}, PTS={}, " + "fpga_timestamp={}, time={:02}:{:02}:{:02}", + gst_buffer_get_size(buffer), + width, + height, + buffer_pts, + xdaqmetadata->fpga_timestamp, + hours, + minutes, + seconds + ); + return GST_FLOW_OK; } void handle_sigint(int) { - spdlog::info("SIGINT received, stopping camera..."); - if (stream_cam) { - stream_cam->stop(); - } + std::println("SIGINT received, stopping stream..."); if (record) { xvc::stop_jpeg_recording(GST_PIPELINE(pipeline)); } + if (stream_cam) { + stream_cam->stop(); + } if (pipeline) { gst_element_set_state(pipeline, GST_STATE_NULL); } if (loop) { g_main_loop_quit(loop); } - std::exit(EXIT_SUCCESS); } } // namespace @@ -106,24 +110,33 @@ int func(int argc, char *argv[]) std::string host = "192.168.177.100"; int id; - std::string cap, codec; + std::string gst_cap; + Codec codec{Codec::MJPEG}; + // TODO + std::unordered_map codec_map{{"mjpeg", Codec::MJPEG}}; - std::string location = "."; + std::string location = "records"; auto split = false; - auto max_size_time = 5; - auto max_files = 10; - auto test = false; + auto max_size_time = 10; std::string log_file; - std::string time_unit; + TimeUnit time_unit{TimeUnit::Seconds}; + std::unordered_map time_unit_map = { + {"seconds", TimeUnit::Seconds}, + {"minutes", TimeUnit::Minutes}, + {"hours", TimeUnit::Hours}, + {"days", TimeUnit::Days} + }; auto stream = app.add_subcommand("stream", "Stream camera"); stream->add_option("--host", host, "Host computer that connected cameras") ->default_val(host) ->group("Stream"); stream->add_option("-i,--id", id, "Camera device ID")->required()->group("Stream"); - stream->add_option("--cap", cap, "Camera capability")->required()->group("Stream"); - stream->add_option("--codec", codec, "Camera codec")->required()->group("Stream"); - stream->add_flag("-t,--test", test, "Enable test mode")->default_val(test)->group("Stream"); + stream->add_option("--cap", gst_cap, "Camera capability")->required()->group("Stream"); + stream->add_option("--codec", codec, "Camera codec") + ->required() + ->transform(CLI::CheckedTransformer(codec_map, CLI::ignore_case)) + ->group("Stream"); auto opt_record = stream->add_flag("-r,--record", record, "Whether to record stream")->group("Record"); @@ -135,24 +148,18 @@ int func(int argc, char *argv[]) ->group("Record"); auto opt_max_size_time = stream - ->add_option("--max-size-time", max_size_time, "Max recording time per file (minutes)") - ->default_val(5) + ->add_option("--max-size-time", max_size_time, "Max recording time per file (seconds)") + ->default_val(10) ->group("Split"); auto opt_time_unit = stream->add_option("--time-unit", time_unit, "Time unit for recording split size") - ->check(CLI::IsMember({"seconds", "minutes", "hours", "days"})) - ->default_val("minutes") - ->group("Split"); - auto opt_max_files = - stream->add_option("--max-files", max_files, "Maximum number of files to keep") - ->default_val(10) + ->transform(CLI::CheckedTransformer(time_unit_map, CLI::ignore_case)) ->group("Split"); opt_location->needs(opt_record); opt_split->needs(opt_record); opt_max_size_time->needs(opt_split); opt_time_unit->needs(opt_split); - opt_max_files->needs(opt_split); auto list = app.add_subcommand("list", "List cameras"); list->add_option("--host", host, "Host computer that connected cameras")->default_val(host); @@ -166,86 +173,60 @@ int func(int argc, char *argv[]) signal(SIGINT, handle_sigint); gst_init(&argc, &argv); - xvc::TimeUnit unit; - - if (time_unit == "seconds") - unit = xvc::TimeUnit::Seconds; - else if (time_unit == "minutes") - unit = xvc::TimeUnit::Minutes; - else if (time_unit == "hours") - unit = xvc::TimeUnit::Hours; - else if (time_unit == "days") - unit = xvc::TimeUnit::Days; - else { - fmt::println("Invalid time unit specified."); - return EXIT_FAILURE; - } - if (*stream) { - // TODO: support h264, h265 - auto valid_codecs = {"jpeg"}; - if (std::find(valid_codecs.begin(), valid_codecs.end(), codec) == valid_codecs.end()) { - fmt::println("Invalid codec. Valid options is: jpeg."); - return EXIT_FAILURE; - } - - if (!test) { - cams = Camera::cameras(); - for (auto cam : cams) { - if (id == cam->id()) { - stream_cam = cam; - break; - } - } - if (!stream_cam) { - fmt::println("Error: no camera with id = {}", id); - return EXIT_FAILURE; - } + handler = std::make_unique(); + pipeline = gst_pipeline_new(nullptr); + loop = g_main_loop_new(nullptr, false); + stream_duration = std::chrono::steady_clock::now(); - auto caps = stream_cam->caps(); - auto it = std::find_if(caps.begin(), caps.end(), [cap](const Camera::Cap &_cap) { - return _cap.to_string() == cap; - }); - if (it == caps.end()) { - fmt::println("Error: Camera {} does not support cap '{}'", id, cap); - return EXIT_FAILURE; + for (auto &camera : Camera::cameras()) { + if (id == camera->id()) { + stream_cam = std::move(camera); + break; } - stream_cam->set_test(test); - stream_cam->start(*it); - } else { - stream_cam = new Camera(id, "test"); - stream_cam->set_test(test); - stream_cam->start(Camera::Cap{ - .media_type = "image/jpeg", - .width = 1920, - .height = 1080, - .fps_n = 30, - }); + } + if (!stream_cam) { + std::println("Error: no camera with id = {}", id); + return -1; } - auto uri = fmt::format("{}:{}", host, stream_cam->port()); - auto record_path = std::filesystem::current_path(); - auto filepath = record_path / fmt::format("{}-{}", stream_cam->name(), stream_cam->id()); - - if (location != ".") { - record_path = fs::path(location); - if (!fs::exists(record_path)) { - fmt::println("Error: specified location path '{}' does not exist.", location); - return EXIT_FAILURE; - } + auto caps = stream_cam->caps(); + auto it = std::find_if(caps.begin(), caps.end(), [gst_cap](const Camera::Cap &_cap) { + return _cap.to_string() == gst_cap; + }); + if (it == caps.end()) { + std::println("Error: Camera {} does not support cap '{}'", id, gst_cap); + return -1; + } + stream_cam->start(*it); + + std::chrono::seconds duration; + switch (time_unit) { + case TimeUnit::Seconds: duration = std::chrono::seconds(max_size_time); break; + case TimeUnit::Minutes: duration = std::chrono::minutes(max_size_time); break; + case TimeUnit::Hours: duration = std::chrono::hours(max_size_time); break; + case TimeUnit::Days: duration = std::chrono::days(max_size_time); break; + default: std::println("Invalid time unit specified."); return -1; } - handler = new MetadataHandler(); - pipeline = gst_pipeline_new(codec.c_str()); - loop = g_main_loop_new(nullptr, false); + auto uri = std::format("{}:{}", host, stream_cam->port()); - if (codec == "jpeg") { + if (codec == Codec::MJPEG) { xvc::setup_jpeg_srt_stream(GST_PIPELINE(pipeline), uri); - if (record) { - xvc::start_jpeg_recording( - GST_PIPELINE(pipeline), filepath, !split, max_size_time, unit, max_files + } + + if (record) { + const auto &base = (location == "records") ? fs::path("records") : fs::path(location); + std::error_code ec; + if (!fs::exists(base) && !fs::create_directories(base, ec)) { + std::println( + "Error: cannot create directory '{}': {}", base.string(), ec.message() ); + return -1; } + auto filepath = base / stream_cam->name(); + xvc::RecordConfig config(filepath, split, duration); + xvc::start_jpeg_recording(GST_PIPELINE(pipeline), config); } auto parser = gst_bin_get_by_name(GST_BIN(pipeline), "parser"); @@ -253,54 +234,50 @@ int func(int argc, char *argv[]) gst_element_get_static_pad(parser, "src"), gst_object_unref ); gst_pad_add_probe( - src_pad.get(), GST_PAD_PROBE_TYPE_BUFFER, parse_jpeg_metadata, handler, nullptr + src_pad.get(), GST_PAD_PROBE_TYPE_BUFFER, parse_jpeg_metadata, handler.get(), nullptr ); GstAppSinkCallbacks callbacks = {nullptr, nullptr, draw_image, nullptr, nullptr, {nullptr}}; auto appsink = gst_bin_get_by_name(GST_BIN(pipeline), "appsink"); gst_app_sink_set_callbacks(GST_APP_SINK(appsink), &callbacks, nullptr, nullptr); - auto ret = gst_element_set_state(pipeline, GST_STATE_PLAYING); - if (ret == GST_STATE_CHANGE_FAILURE) { - spdlog::error("Unable to set the pipeline to the playing state"); - return EXIT_FAILURE; + if (gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { + std::println("Unable to set the pipeline to the playing state"); + return -1; } - auto _thread = std::jthread([]() { - spdlog::debug("Run GStreamer stream thread"); - g_main_loop_run(loop); - spdlog::debug("Quit GStreamer stream thread"); - delete stream_cam; - delete handler; - stream_cam = nullptr; - handler = nullptr; - }); + g_main_loop_run(loop); + + if (pipeline) { + gst_element_set_state(pipeline, GST_STATE_NULL); + } + stream_cam.reset(); + handler.reset(); } if (*list) { - cams = Camera::cameras(); - fmt::println("Discovered Cameras:"); - - for (auto cam : cams) { - fmt::println(""); - fmt::println("Camera ID : {}", cam->id()); - fmt::println("Name : {}", cam->name()); - fmt::println("Capabilities :"); - - for (auto cap : cam->caps()) { - fmt::println(" - {}", cap.to_string()); + std::println("Discovered Cameras:"); + for (const auto &camera : Camera::cameras()) { + std::println(""); + std::println("Camera ID : {}", camera->id()); + std::println("Device ID : {}", camera->device_id()); + std::println("Name : {}", camera->name()); + + std::println("Capabilities:"); + for (const auto &cap : camera->caps()) { + std::println(" - {}", cap.to_string()); } } } if (*logs) { - auto server = xvc::Server(host); - auto logs = log_file.empty() ? server.logs() : server.logs(log_file); - - fmt::println("{}", logs); + auto server = xvc::Server(); + if (auto logs = log_file.empty() ? server.logs() : server.logs(log_file)) { + std::println("{}", logs.value()); + } } - return EXIT_SUCCESS; + return 0; } int main(int argc, char *argv[]) diff --git a/tool/xvc_update_tool.cc b/tool/xvc_update_tool.cc index 50d23fd..bfe46b3 100644 --- a/tool/xvc_update_tool.cc +++ b/tool/xvc_update_tool.cc @@ -3,6 +3,7 @@ #include #include +#include "common.h" #include "updater.h" int main(int argc, char *argv[]) diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 0415c53..39b5c8b 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -1,69 +1,73 @@ -add_library(libxvc STATIC) +add_library(xvc STATIC) -set(XVC_SOURCES +set(sources xvc.cc camera.cc port_pool.cc ws_client.cc server.cc - updater.cc + validator.cc ) -set(XVC_HEADERS +set(headers xvc.h camera.h port_pool.h ws_client.h server.h - updater.h + validator.h + common.h ) -target_sources(libxvc +target_sources(xvc PRIVATE - "${XVC_SOURCES}" + "${sources}" PUBLIC FILE_SET "public_headers" TYPE "HEADERS" - FILES "${XVC_HEADERS}" + FILES "${headers}" ) -target_include_directories(libxvc +target_include_directories(xvc INTERFACE "$" "$" ) -set_target_properties(libxvc PROPERTIES +set_target_properties(xvc PROPERTIES VERSION "${libxvc_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}" POSITION_INDEPENDENT_CODE ON ) -target_compile_features(libxvc PUBLIC cxx_std_20) -target_compile_options(libxvc +target_compile_features(xvc PUBLIC cxx_std_23) +target_compile_options(xvc PRIVATE $<$:/W4> - # $<$:/W4 /WX> $<$>:-Wall> - # $<$>:-Wall -Wextra -Wpedantic -Werror> ) if(CMAKE_BUILD_TYPE MATCHES "Debug") - target_compile_options(libxvc PUBLIC -fsanitize=address,undefined) - target_link_options(libxvc PUBLIC -fsanitize=address,undefined) + target_compile_options(xvc PUBLIC -fsanitize=address,undefined) + target_link_options(xvc PUBLIC -fsanitize=address,undefined) + # target_compile_options(xvc PUBLIC -fsanitize=thread) + # target_link_options(xvc PUBLIC -fsanitize=thread) endif() -target_link_libraries(libxvc +target_link_libraries(xvc PUBLIC - fmt::fmt cpr::cpr spdlog::spdlog nlohmann_json::nlohmann_json + nlohmann_json_schema_validator PkgConfig::gstreamer xdaqmetadata::xdaqmetadata Boost::boost - OpenSSL::SSL ) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "default install path" FORCE) +endif() + install( - TARGETS libxvc + TARGETS xvc EXPORT libxvc-targets LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" @@ -80,4 +84,28 @@ export( EXPORT libxvc-targets FILE libxvc-config.cmake NAMESPACE libxvc:: +) + +add_library(updater STATIC) + +target_sources(updater + PRIVATE + updater.cc + PUBLIC + FILE_SET "public_headers" + TYPE "HEADERS" + FILES updater.h +) +target_compile_features(updater PUBLIC cxx_std_23) +target_compile_options(updater + PRIVATE + $<$:/W4> + $<$>:-Wall> +) +target_link_libraries(updater + PUBLIC + cpr::cpr + spdlog::spdlog + nlohmann_json::nlohmann_json + OpenSSL::SSL ) \ No newline at end of file diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index 9172d27..b52f3d4 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -3,23 +3,27 @@ #include #include +#include #include #include "port_pool.h" +#include "validator.h" + +using nlohmann::json; namespace { -constexpr auto Cameras = "http://192.168.177.100:8000/cameras"; -constexpr auto MJPEG = "http://192.168.177.100:8000/jpeg"; -constexpr auto Test = "http://192.168.177.100:8000/test"; -constexpr auto H265 = "http://192.168.177.100:8000/h265"; -constexpr auto H264 = "http://192.168.177.100:8000/h264"; -constexpr auto Stop = "http://192.168.177.100:8000/stop"; +constexpr std::string_view URL = "http://192.168.177.100:8000"; +constexpr std::string_view CAMERAS = "/cameras"; +constexpr std::string_view MJPEG = "/jpeg"; +constexpr std::string_view H265 = "/h265"; +constexpr std::string_view H264 = "/h264"; +constexpr std::string_view STOP = "/stop"; constexpr auto OK = 200; PortPool pool(9000, 9064); -std::optional get_json(std::string_view url, std::chrono::milliseconds timeout) +std::optional get_json(std::string_view url, std::chrono::milliseconds timeout) { if (url.empty()) { spdlog::error("GET attempted with empty URL"); @@ -29,13 +33,13 @@ std::optional get_json(std::string_view url, std::chrono::milliseconds tim auto res = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); if (res.status_code != OK) { spdlog::warn( - "GET {} failed (status={}, error='{}')", url, res.status_code, res.error.message + "Failed to GET {} (status={}, error='{}')", url, res.status_code, res.error.message ); return std::nullopt; } try { - return json::parse(res.text); + return nlohmann::json::parse(res.text); } catch (const std::exception &e) { spdlog::error("JSON parse error from {}: {}", url, e.what()); return std::nullopt; @@ -43,7 +47,7 @@ std::optional get_json(std::string_view url, std::chrono::milliseconds tim }; cpr::Response post_json( - std::string_view url, const json &payload, const std::chrono::milliseconds timeout + std::string_view url, const nlohmann::json &payload, std::chrono::milliseconds timeout ) { if (url.empty()) { @@ -68,112 +72,142 @@ void log(const cpr::Response &r, std::string_view action) } } +constexpr std::string url(std::string_view endpoint) { return std::format("{}{}", URL, endpoint); } + } // namespace -Camera::Camera(const int id, std::string_view device_id, std::string_view name) - : _id(id), _device_id(device_id), _name(name), _test(false) +Camera::Camera(int id, std::string device_id, std::string name) + : _id(id), _device_id(std::move(device_id)), _name(std::move(name)) { - if (auto port = pool.allocate_port()) { - _port = port.value(); + auto port = pool.allocate(); + if (!port) { + spdlog::error("Failed to allocate port for camera id: {}", _id); + throw std::runtime_error("Failed to allocate port for camera"); } - spdlog::info( + + _port = port.value(); + spdlog::debug( "Creating camera id: {}, device_id: {}, name: {}, port: {}", _id, _device_id, _name, _port ); } Camera::~Camera() { - pool.release_port(_port); - spdlog::info( - "Deleting camera id: {}, device_id: {}, name: {}, port: {}", _id, _device_id, _name, _port - ); + if (pool.release(_port)) { + spdlog::debug( + "Deleting camera id: {}, device_id: {}, name: {}, port: {}", + _id, + _device_id, + _name, + _port + ); + } } -std::vector Camera::cameras(const std::chrono::milliseconds duration) +std::unique_ptr Camera::parse(std::string_view camera_json) { - std::vector cameras; + try { + auto json = json::parse(camera_json); + if (json.empty() || !json.is_object()) { + spdlog::error("Invalid camera JSON format"); + return nullptr; + } + if (auto validated = validate_camera(json); !validated) { + spdlog::error("Camera JSON validation failed: {}", validated.error()); + return nullptr; + } - auto data = get_json(Cameras, duration); - if (!data) { - return cameras; - } - cameras.reserve(data->size()); + auto camera = std::make_unique( + json.at("id").get(), + json.at("device_id").get(), + json.at("name").get() + ); - for (const auto &cam_json : *data) { - if (auto cam = parse(cam_json)) { - cameras.emplace_back(cam); + for (const auto &cap_json : json.at("caps")) { + Camera::Cap cap{ + .media_type = cap_json.at("media_type").get(), + .format = cap_json.value("format", ""), + .width = cap_json.at("width").get(), + .height = cap_json.at("height").get() + }; + + const auto &framerate = cap_json.at("framerate").get(); + auto slash = framerate.find('/'); + if (slash != std::string::npos) { + cap.fps_n = std::stoi(framerate.substr(0, slash)); + cap.fps_d = std::stoi(framerate.substr(slash + 1)); + } + + if (cap.media_type == "image/jpeg") { + camera->add_cap(cap); + } } + return camera; + } catch (const std::exception &e) { + spdlog::error("Exception parsing camera JSON: {}", e.what()); + return nullptr; } - - return cameras; } -// std::unique_ptr Camera::parse(const json &camera_json) -Camera *Camera::parse(const json &camera_json) +std::vector> Camera::cameras(std::chrono::milliseconds duration) { - auto const id = camera_json["id"].get(); - auto const device_id = camera_json["device_id"].get(); - auto const name = camera_json["name"].get(); - auto const caps_json = camera_json["caps"]; - - auto camera = new Camera(id, device_id, name); - // auto camera = std::make_unique( - // camera_json["id"].get(), camera_json["name"].get() - // ); - - for (const auto &cap_json : caps_json) { - Camera::Cap cap{ - .media_type = cap_json.at("media_type").get(), - .format = cap_json.at("format").get(), - .width = cap_json.at("width").get(), - .height = cap_json.at("height").get() - }; - - auto framerate_str = cap_json.at("framerate").get(); - auto delimiter_pos = framerate_str.find('/'); - if (delimiter_pos != std::string::npos) { - cap.fps_n = std::stoi(framerate_str.substr(0, delimiter_pos)); - cap.fps_d = std::stoi(framerate_str.substr(delimiter_pos + 1)); - } + std::vector> cameras; - if (cap.media_type != "image/jpeg") { + auto camera_json = get_json(url(CAMERAS), duration); + if (!camera_json || !camera_json->is_array()) { + spdlog::error("Invalid cameras format"); + return cameras; + } + cameras.reserve(camera_json->size()); + + for (const auto &json : *camera_json) { + if (auto validated = validate_camera(json); !validated) { + spdlog::error("Camera JSON validation failed: {}", validated.error()); continue; } - camera->add_cap(cap); + if (auto cam = parse(json.dump())) { + cameras.emplace_back(std::move(cam)); + } } - - return camera; + return cameras; } -void Camera::start(const Cap &cap, const std::chrono::milliseconds duration) +bool Camera::start(const Cap &cap, std::chrono::milliseconds duration) { - const json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; - - std::string_view url; + const nlohmann::json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; - if (_test) { - url = Test; - } else if (cap.media_type == "image/jpeg") { - url = MJPEG; + std::string _url; + if (cap.media_type == "image/jpeg") { + _url = url(MJPEG); } else if (cap.media_type == "video/x-h265") { - url = H265; + _url = url(H265); } else if (cap.media_type == "video/x-h264") { - url = H264; + _url = url(H264); } else { spdlog::error("Unsupported codec for camera id: {}", _id); - return; + return false; } - auto res = post_json(url, payload, duration); + auto res = post_json(_url, payload, duration); + if (res.status_code != OK) { + log(res, std::format("Start camera id: {}", _id)); + return false; + } - log(res, fmt::format("Start camera id: {}", _id)); + log(res, std::format("Start camera id: {}", _id)); + return true; } -void Camera::stop(const std::chrono::milliseconds duration) +bool Camera::stop(std::chrono::milliseconds duration) { - const json payload{{"id", _id}}; + const nlohmann::json payload{{"id", _id}}; - auto res = post_json(Stop, payload, duration); + auto res = post_json(url(STOP), payload, duration); + if (res.status_code != OK) { + log(res, std::format("Stop camera id: {}", _id)); + return false; + } - log(res, fmt::format("Stop camera id: {}", _id)); + log(res, std::format("Stop camera id: {}", _id)); + return true; } \ No newline at end of file diff --git a/xdaqvc/camera.h b/xdaqvc/camera.h index 24bef0e..781186b 100644 --- a/xdaqvc/camera.h +++ b/xdaqvc/camera.h @@ -1,21 +1,14 @@ #pragma once -#include - #include -#include +#include #include #include #include -using namespace std::chrono_literals; -using nlohmann::json; - class Camera { public: - // video/x-raw,format=YUY2,width=640,height=480,framerate=30/1 - // image/jpeg,width=640,height=480,framerate=30/1 struct Cap { std::string media_type; std::optional format = std::nullopt; @@ -24,10 +17,10 @@ class Camera int fps_n; int fps_d; - std::string to_string() const + constexpr std::string to_string() const noexcept { if (format.has_value() && !format.value().empty()) { - return fmt::format( + return std::format( "{},format={},width={},height={},framerate={}/{}", media_type, format.value(), @@ -37,62 +30,38 @@ class Camera fps_d ); } else { - return fmt::format( + return std::format( "{},width={},height={},framerate={}/{}", media_type, width, height, fps_n, fps_d ); } } }; - // enum class Codec { MJPEG, H265, H264 }; - Camera(const int id = -1, std::string_view device_id = "", std::string_view name = ""); + explicit Camera(int id, std::string device_id, std::string name); ~Camera(); - // [[nodiscard]] static std::unique_ptr parse(const json &event); - [[nodiscard]] static Camera *parse(const json &event); - - // [[nodiscard]] static std::vector> cameras( - // const std::chrono::milliseconds duration = 500ms - // ); - [[nodiscard]] static std::vector cameras( - const std::chrono::milliseconds duration = 1s + // TODO: Camera::parse is used by xvc::ws_client function pointer and Camera::cameras + // but it should be made private. + [[nodiscard]] static std::unique_ptr parse(std::string_view camera_json); + [[nodiscard]] static std::vector> cameras( + std::chrono::milliseconds timeout = std::chrono::milliseconds(1000) ); - [[nodiscard]] int id() const { return _id; } - [[nodiscard]] std::string device_id() const { return _device_id; } - [[nodiscard]] unsigned short port() const { return _port; } + [[nodiscard]] int id() const noexcept { return _id; } + [[nodiscard]] const std::string &device_id() const noexcept { return _device_id; } + [[nodiscard]] const std::string &name() const noexcept { return _name; } + [[nodiscard]] unsigned short port() const noexcept { return _port; } + [[nodiscard]] const std::vector &caps() const noexcept { return _caps; } - [[nodiscard]] std::string name() const { return _name; } void set_name(std::string_view name) { _name = name; } - - [[nodiscard]] std::vector caps() const { return _caps; } void add_cap(const Cap &cap) { _caps.emplace_back(cap); } - // [[nodiscard]] std::vector codecs() const { return _codecs; } - // void add_codec(const Codec &codec) - // { - // if (std::find(_codecs.begin(), _codecs.end(), codec) == _codecs.end()) { - // _codecs.emplace_back(codec); - // } - // } - - // [[nodiscard]] Codec stream_codec() const { return _stream_codec; } - // void set_stream_codec(const Codec &codec) { _stream_codec = codec; } - - void start(const Cap &cap, std::chrono::milliseconds duration = 500ms); - void stop(const std::chrono::milliseconds duration = 500ms); - - [[nodiscard]] bool test_mode() const { return _test; } - void set_test(const bool test) { _test = test; } + bool start(const Cap &cap, std::chrono::milliseconds duration = std::chrono::milliseconds(1000)); + bool stop(std::chrono::milliseconds duration = std::chrono::milliseconds(1000)); private: int _id; std::string _device_id; unsigned short _port; std::string _name; - std::vector _caps; - // std::vector _codecs; - // Codec _stream_codec; - - bool _test; }; \ No newline at end of file diff --git a/xdaqvc/common.h b/xdaqvc/common.h new file mode 100644 index 0000000..fd300ce --- /dev/null +++ b/xdaqvc/common.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include + +namespace xvc +{ + +class Version +{ +public: + constexpr Version() noexcept : _major(0), _minor(0), _patch(0) {} + constexpr Version(int major, int minor, int patch) noexcept + : _major(major), _minor(minor), _patch(patch) + { + } + + constexpr bool operator==(const Version &other) const noexcept + { + return _major == other._major && _minor == other._minor && _patch == other._patch; + } + constexpr bool operator>(const Version &other) const noexcept + { + return !(*this < other || *this == other); + } + constexpr bool operator<(const Version &other) const noexcept + { + if (_major != other._major) return _major < other._major; + if (_minor != other._minor) return _minor < other._minor; + return _patch < other._patch; + } + constexpr bool operator>=(const Version &other) const noexcept { return !(*this < other); } + constexpr bool operator<=(const Version &other) const noexcept + { + return (*this < other) || (*this == other); + }; + + // TODO + [[nodiscard]] static std::optional from_string(std::string_view version) + { + try { + std::regex regex(R"((\d+)\.(\d+)\.(\d+))"); + std::smatch matches; + std::string version_str(version); + + if (std::regex_match(version_str, matches, regex)) { + return Version{ + std::stoi(matches[1].str()), + std::stoi(matches[2].str()), + std::stoi(matches[3].str()) + }; + } + return std::nullopt; + } catch (...) { + return std::nullopt; + } + }; + + [[nodiscard]] constexpr std::string to_string() const noexcept + { + return std::format("{}.{}.{}", _major, _minor, _patch); + }; + +private: + int _major; + int _minor; + int _patch; +}; + +} // namespace xvc \ No newline at end of file diff --git a/xdaqvc/port_pool.cc b/xdaqvc/port_pool.cc index c30ce43..7034611 100644 --- a/xdaqvc/port_pool.cc +++ b/xdaqvc/port_pool.cc @@ -2,7 +2,6 @@ #include -#include #include PortPool::PortPool(Port start, Port end) : _start(start), _end(end) @@ -11,101 +10,100 @@ PortPool::PortPool(Port start, Port end) : _start(start), _end(end) throw std::invalid_argument("Invalid port range"); } - for (auto port = start; port < end; ++port) { + _available_ports.reserve(_end - _start); + for (auto port = _start; port < _end; ++port) { _available_ports.insert(port); + _shuffled_ports.push_back(port); } + + std::random_device rd; + std::mt19937 gen(rd()); + std::shuffle(_shuffled_ports.begin(), _shuffled_ports.end(), gen); } PortPool::~PortPool() { for (auto &[port, acceptor] : _bound_ports) { boost::system::error_code ec; - acceptor->close(ec); + auto _ = acceptor->close(ec); if (ec) { - spdlog::warn("Failed to close acceptor on port {}: {}", port, ec.message()); + spdlog::error("Failed to close acceptor on port {}: {}", port, ec.message()); } } + _bound_ports.clear(); } -std::optional PortPool::allocate_port() +std::optional PortPool::allocate() { if (_available_ports.empty()) { spdlog::warn("No available ports"); return std::nullopt; } - std::vector ports(_available_ports.begin(), _available_ports.end()); - - std::random_device rd; - std::mt19937 gen(rd()); - std::shuffle(ports.begin(), ports.end(), gen); - - for (auto port : ports) { - boost::system::error_code ec; - - auto acceptor = std::make_shared(_io_context); - acceptor->open(boost::asio::ip::tcp::v4(), ec); - if (ec) { - spdlog::debug("Failed to open acceptor on port {}: {}", port, ec.message()); - continue; - } - - acceptor->set_option(boost::asio::ip::tcp::acceptor::reuse_address(true), ec); - if (ec) { - spdlog::debug("Failed to set reuse_address on port {}: {}", port, ec.message()); + for (auto port : _shuffled_ports) { + if (!_available_ports.contains(port)) { continue; } - - auto _ = acceptor->bind({boost::asio::ip::tcp::v4(), port}, ec); - if (ec == boost::asio::error::address_in_use) { - spdlog::debug("Failed to bind port {}: {}", port, ec.message()); - continue; + if (try_bind(port)) { + _available_ports.erase(port); + spdlog::debug("Allocated port {}", port); + return port; } - - _bound_ports[port] = acceptor; - _available_ports.erase(port); - - spdlog::info("Allocated port {}", port); - return port; } - - spdlog::warn("No ports could be bound successfully"); + spdlog::error("No ports could be bound"); return std::nullopt; } -void PortPool::release_port(Port port) +bool PortPool::release(Port port) { - // if (_start <= port && port < _end) { - // _available_ports.insert(port); - // _bound_ports.erase(port); - // fmt::println("Released and unbound port {}", port); - // } if (port < _start || port >= _end) { - spdlog::warn("Attempted to release port {} outside of range", port); - return; + spdlog::warn("Failed to release port {}: outside of range", port); + return false; } auto it = _bound_ports.find(port); - if (it != _bound_ports.end()) { - boost::system::error_code ec; - it->second->close(ec); - if (ec) { - spdlog::warn("Failed to close acceptor on port {}: {}", port, ec.message()); - } - - _bound_ports.erase(it); + if (it == _bound_ports.end()) { + spdlog::warn("Failed to release port {}: not allocated", port); + return false; } + boost::system::error_code ec; + auto _ = it->second->close(ec); + if (ec) { + spdlog::error("Failed to close acceptor on port {}: {}", port, ec.message()); + } + _bound_ports.erase(it); _available_ports.insert(port); - spdlog::info("Released port {}", port); - // fmt::println("Port {} is not in the valid range", port); + + spdlog::debug( + "Released port {} ({}/{} ports in use)", port, _bound_ports.size(), _end - _start + ); + return true; } -void PortPool::print_available_ports() const +bool PortPool::try_bind(Port port) { - std::string ports; - for (const auto port : _available_ports) { - ports += std::to_string(port) + " "; + boost::system::error_code ec; + auto acceptor = std::make_unique(_io_context); + + auto _ = acceptor->open(tcp::v4(), ec); + if (ec) { + spdlog::error("Failed to open acceptor on port {}: {}", port, ec.message()); + return false; + } + + _ = acceptor->set_option(Acceptor::reuse_address(true), ec); + if (ec) { + spdlog::error("Failed to set reuse_address option on port {}: {}", port, ec.message()); + return false; } - spdlog::info("Available Ports: {}", ports); + + _ = acceptor->bind({tcp::v4(), port}, ec); + if (ec == boost::asio::error::address_in_use) { + spdlog::error("Failed to bind port {}: {}", port, ec.message()); + return false; + } + + _bound_ports.emplace(port, std::move(acceptor)); + return true; } \ No newline at end of file diff --git a/xdaqvc/port_pool.h b/xdaqvc/port_pool.h index bd6275a..387f20d 100644 --- a/xdaqvc/port_pool.h +++ b/xdaqvc/port_pool.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,17 +11,34 @@ class PortPool public: using Port = unsigned short; + // Range: [start, end) explicit PortPool(Port start, Port end); ~PortPool(); - [[nodiscard]] std::optional allocate_port(); - void release_port(Port port); - void print_available_ports() const; + PortPool(const PortPool &) = delete; + PortPool(PortPool &&) = delete; + PortPool &operator=(const PortPool &) = delete; + PortPool &operator=(PortPool &&) = delete; + + [[nodiscard]] std::optional allocate(); + bool release(Port port); + + [[nodiscard]] const std::unordered_set &available_ports() const noexcept + { + return _available_ports; + }; private: - std::unordered_map> _bound_ports; + using tcp = boost::asio::ip::tcp; + using Acceptor = tcp::acceptor; + std::unordered_map> _bound_ports; std::unordered_set _available_ports; - Port _start, _end; + std::vector _shuffled_ports; + + bool try_bind(Port port); + + Port _start; + Port _end; boost::asio::io_context _io_context; }; diff --git a/xdaqvc/server.cc b/xdaqvc/server.cc index a5185a3..ed3b70b 100644 --- a/xdaqvc/server.cc +++ b/xdaqvc/server.cc @@ -1,147 +1,74 @@ #include "server.h" #include -#include #include +#include #include -using json = nlohmann::json; - namespace { -auto constexpr OK = 200; +constexpr int OK = 200; } // namespace namespace xvc { -Version::Version(const std::string &version_str) : major(0), minor(0), patch(0) -{ - std::istringstream ss(version_str); - std::string token; - std::vector parts; - - while (std::getline(ss, token, '.')) { - try { - parts.push_back(std::stoi(token)); - } catch (const std::invalid_argument &) { - throw std::invalid_argument("Invalid version format: " + version_str); - } - } - - if (parts.size() != 3) { - throw std::invalid_argument( - "Version must have exactly three components (major.minor.patch): " + version_str - ); - } - - major = parts[0]; - minor = parts[1]; - patch = parts[2]; -} - -bool Version::operator==(const Version &other) const -{ - return major == other.major && minor == other.minor && patch == other.patch; -} - -bool Version::operator<(const Version &other) const +Server::Server(std::string_view host, int port) noexcept + : _base_url(std::format("http://{}:{}", host, port)) { - if (major != other.major) return major < other.major; - if (minor != other.minor) return minor < other.minor; - return patch < other.patch; } -bool Version::operator>(const Version &other) const { return other < *this; } - -std::string Version::to_string() const { return fmt::format("{}.{}.{}", major, minor, patch); } - -Server::Server(const std::string &host, int port) - : _base_url(fmt::format("http://{}:{}", host, port)) -{ -} - -Status Server::status(const std::chrono::milliseconds timeout) const +bool Server::root(std::chrono::milliseconds timeout) const { auto response = cpr::Get(cpr::Url{_base_url}, cpr::Timeout{timeout}); - - // cpr::Session session; - // session.SetUrl(url); - // session.SetTimeout(_timeout); - // auto response = session.Get(); - - return (response.status_code == OK) ? Status::ON : Status::OFF; + if (response.status_code != OK) { + spdlog::error("Failed to fetch {}. Status: {}", _base_url, response.status_code); + return false; + } + return true; } -std::string Server::logs(const std::string &filename, const std::chrono::milliseconds timeout) const +std::optional Server::logs( + std::string_view filename, std::chrono::milliseconds timeout +) const { - auto url = fmt::format("{}/{}", _base_url, "logs"); - if (!filename.empty()) { - url += "/" + filename; - } - spdlog::debug("Fetching logs from = {}", url); + const auto logs = filename.empty(); + const auto &url = logs ? std::format("{}/{}", _base_url, "logs") + : std::format("{}/{}/{}", _base_url, "logs", filename); auto response = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); if (response.status_code != OK) { - spdlog::error("Failed to fetch logs. Status: {}", response.status_code); - return ""; + spdlog::error("Failed to fetch {}. Status: {}", url, response.status_code); + return std::nullopt; } - - return response.text; + return logs ? nlohmann::json::parse(response.text).dump(2) : response.text; } -std::optional Server::get_api_version(const std::chrono::milliseconds timeout) const +std::optional Server::api_version(std::chrono::milliseconds timeout) const { - auto url = fmt::format("{}/{}", _base_url, "version"); - auto response = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); + const auto &url = std::format("{}/{}", _base_url, "api_version"); + auto response = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); if (response.status_code != OK) { - spdlog::warn("Failed to fetch API version. Status: {}", response.status_code); + spdlog::error("Failed to fetch {}. Status: {}", url, response.status_code); return std::nullopt; } - return response.text; -} -bool Server::update(const Version &update_version, const std::chrono::milliseconds timeout) const -{ - auto api_version = get_api_version(); - if (!api_version) { - spdlog::error("Failed to get server API version."); - return false; - } - - Version current_version{json::parse(api_version.value())["version"].get()}; - - if (current_version == update_version) { - spdlog::info( - "Skip update: current version {} = update version {}", - current_version.to_string(), - update_version.to_string() - ); - return true; + auto text = response.text; + try { + auto json = nlohmann::json::parse(text); + auto version_str = json.at("version").get(); + auto version = Version::from_string(version_str); + if (!version) { + spdlog::error("Invalid API version format: {}", text); + return std::nullopt; + } + return version.value(); + } catch (const nlohmann::json::parse_error &e) { + spdlog::error("Failed to parse {} response as JSON: {}", url, text); + return std::nullopt; } - - // TODO - // json payload = {{"version", update_version.to_string()}}; - - // auto url = fmt::format("{}/{}", _base_url, "update"); - - // auto response = cpr::Post( - // cpr::Url{url}, - // cpr::Header{{"Content-Type", "application/json"}}, - // cpr::Body{payload.dump()}, - // cpr::Timeout{timeout} - // ); - - // if (response.status_code != OK) { - // spdlog::error("Update request failed: {} - {}", response.status_code, response.text); - // return false; - // } - // - - spdlog::info("API version updated successfully."); - return true; } } // namespace xvc \ No newline at end of file diff --git a/xdaqvc/server.h b/xdaqvc/server.h index 52005bd..dfcd54c 100644 --- a/xdaqvc/server.h +++ b/xdaqvc/server.h @@ -3,44 +3,28 @@ #include #include #include +#include -using namespace std::chrono_literals; +#include "common.h" namespace xvc { -enum class Status { OFF, ON }; - -class Version -{ -public: - Version(const std::string &version_str); - - bool operator==(const Version &other) const; - bool operator<(const Version &other) const; - bool operator>(const Version &other) const; - - std::string to_string() const; - -private: - int major; - int minor; - int patch; -}; - class Server { public: - Server(const std::string &host = "192.168.177.100", int port = 8000); + explicit Server(std::string_view host = "192.168.177.100", int port = 8000) noexcept; - Status status(const std::chrono::milliseconds timeout = 500ms) const; - std::string logs( - const std::string &filename = "", const std::chrono::milliseconds timeout = 500ms + bool root(std::chrono::milliseconds timeout = std::chrono::milliseconds(500)) const; + + std::optional logs( + std::string_view filename = "", + std::chrono::milliseconds timeout = std::chrono::milliseconds(500) ) const; - std::optional get_api_version(const std::chrono::milliseconds timeout = 500ms) - const; - bool update(const Version &version, const std::chrono::milliseconds timeout = 500ms) const; + std::optional api_version( + std::chrono::milliseconds timeout = std::chrono::milliseconds(500) + ) const; private: std::string _base_url; diff --git a/xdaqvc/updater.cc b/xdaqvc/updater.cc index fac99f3..9941c7c 100644 --- a/xdaqvc/updater.cc +++ b/xdaqvc/updater.cc @@ -6,44 +6,15 @@ #include #include -#include #include using namespace std::chrono_literals; - +namespace fs = std::filesystem; namespace { auto constexpr OK = 200; - -// size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) -// { -// return fwrite(ptr, size, nmemb, stream); -// } - -// std::string bytes_to_hex(const unsigned char *bytes, size_t len) -// { -// std::stringstream ss; -// ss << std::hex << std::setfill('0'); -// for (size_t i = 0; i < len; i++) { -// ss << std::setw(2) << static_cast(bytes[i]); -// } -// return ss.str(); -// } - -// bool handle_response(const cpr::Response &response) -// { -// if (response.status_code == OK) { -// auto json_response = nlohmann::json::parse(response.text); -// return json_response["status"] == "success"; -// } - -// spdlog::error( -// "File transfer failed with status code: {} ({})", response.status_code, response.text -// ); -// return false; -// } } // namespace @@ -648,42 +619,4 @@ UpdateResult update_server( } } -bool Version::operator==(const Version &other) const -{ - return major == other.major && minor == other.minor && patch == other.patch; -} - -bool Version::operator>(const Version &other) const { return !(*this < other || *this == other); } - -bool Version::operator<(const Version &other) const -{ - if (major != other.major) return major < other.major; - if (minor != other.minor) return minor < other.minor; - return patch < other.patch; -} - -bool Version::operator>=(const Version &other) const { return !(*this < other); } - -bool Version::operator<=(const Version &other) const { return (*this < other) || (*this == other); } - -std::optional Version::from_string(const std::string &version_str) -{ - try { - std::regex version_regex(R"((\d+)\.(\d+)\.(\d+))"); - std::smatch matches; - - if (std::regex_match(version_str, matches, version_regex)) { - return Version{ - std::stoi(matches[1].str()), - std::stoi(matches[2].str()), - std::stoi(matches[3].str()) - }; - } - return std::nullopt; - } catch (...) { - return std::nullopt; - } -} - -std::string Version::to_string() const { return fmt::format("{}.{}.{}", major, minor, patch); } } // namespace xvc \ No newline at end of file diff --git a/xdaqvc/updater.h b/xdaqvc/updater.h index dd3f65f..f73945e 100644 --- a/xdaqvc/updater.h +++ b/xdaqvc/updater.h @@ -7,8 +7,7 @@ #include #include - -namespace fs = std::filesystem; +#include "common.h" namespace xvc @@ -19,23 +18,6 @@ struct DownloadResult { std::string error_message; }; -class Version -{ -public: - int major; - int minor; - int patch; - - bool operator==(const Version &other) const; - bool operator>(const Version &other) const; - bool operator<(const Version &other) const; - bool operator>=(const Version &other) const; - bool operator<=(const Version &other) const; - - static std::optional from_string(const std::string &version_str); - std::string to_string() const; -}; - struct HandshakeResponse { bool success; std::string token; @@ -76,7 +58,7 @@ DownloadResult download_and_verify( const std::filesystem::path &output_path ); -std::optional calculate_sha256(const fs::path &filepath); +std::optional calculate_sha256(const std::filesystem::path &filepath); HandshakeResponse perform_handshake(const std::string &server_address, int port); @@ -88,7 +70,7 @@ bool prepare_file_transfer( bool transfer_file( const std::string &server_address, int port, const std::string &token, - const fs::path &file_path, const std::string &transfer_id, + const std::filesystem::path &file_path, const std::string &transfer_id, std::function progress_callback = nullptr ); @@ -103,8 +85,9 @@ UpdateResult update_server( const std::string &server_address, int server_port, // Port of the server to be updated int update_server_port, // Port of the update server - const std::string &table_url, const fs::path &update_dir, const Version &client_version, - bool skip_version_check = false, const std::optional &force_version = std::nullopt + const std::string &table_url, const std::filesystem::path &update_dir, + const Version &client_version, bool skip_version_check = false, + const std::optional &force_version = std::nullopt ); } // namespace xvc \ No newline at end of file diff --git a/xdaqvc/validator.cc b/xdaqvc/validator.cc new file mode 100644 index 0000000..1ea633d --- /dev/null +++ b/xdaqvc/validator.cc @@ -0,0 +1,57 @@ +#include +#include + +#include + +using nlohmann::json; +using nlohmann::json_schema::json_validator; + +static auto camera_schema = R"( +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["id", "device_id", "name", "caps"], + "properties": { + "id": { "type": "integer" }, + "device_id": { "type": "string" }, + "name": { "type": "string" }, + "caps": { + "type": "array", + "items": { + "type": "object", + "required": ["media_type", "width", "height", "framerate"], + "properties": { + "media_type": { "type": "string" }, + "format": { "type": "string" }, + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 }, + "framerate": { + "type": "string", + "pattern": "^[0-9]+/[0-9]+$" + } + } + } + } + } +} +)"_json; + +std::expected validate_camera(const nlohmann::json &json) +{ + static auto validator = [] -> json_validator { + json_validator v; + try { + v.set_root_schema(camera_schema); + } catch (const std::exception &e) { + spdlog::error("Validation of schema failed: {}", e.what()); + } + return v; + }(); + + try { + validator.validate(json); + } catch (const std::exception &e) { + return std::unexpected(e.what()); + } + return {}; +} diff --git a/xdaqvc/validator.h b/xdaqvc/validator.h new file mode 100644 index 0000000..44a8e99 --- /dev/null +++ b/xdaqvc/validator.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +std::expected validate_camera(const nlohmann::json &json); diff --git a/xdaqvc/ws_client.cc b/xdaqvc/ws_client.cc index bbf17d8..44f8c71 100644 --- a/xdaqvc/ws_client.cc +++ b/xdaqvc/ws_client.cc @@ -2,6 +2,8 @@ #include +#include + namespace http = beast::http; // from // Report a failure @@ -11,14 +13,14 @@ void fail(beast::error_code ec, std::string_view what) } session::session( - std::string_view host, std::string_view port, net::io_context &ioc, - std::function event_handler + std::string host, std::string port, net::io_context &ioc, + std::function handler ) : _resolver(net::make_strand(ioc)), _ws(net::make_strand(ioc)), - _host(host), - _port(port), - _handler(std::move(event_handler)) + _host(std::move(host)), + _port(std::move(port)), + _handler(std::move(handler)) { } @@ -69,7 +71,7 @@ void session::on_connect(beast::error_code ec, tcp::resolver::results_type::endp // Update the _host string. This will provide the value of the // Host HTTP header during the WebSocket handshake. // See https://tools.ietf.org/html/rfc7230#section-5.4 - _host = fmt::format("{}:{}", _host, ep.port()); + _host = std::format("{}:{}", _host, ep.port()); // Perform the websocket handshake _ws.async_handshake( @@ -125,7 +127,7 @@ void session::on_close(beast::error_code ec) spdlog::debug("WebSocket closed gracefully"); } -void session::reconnect(const std::chrono::milliseconds timeout) +void session::reconnect(std::chrono::milliseconds timeout) { spdlog::debug("session has been disconnected, trying to reconnect..."); @@ -142,17 +144,11 @@ void session::reconnect(const std::chrono::milliseconds timeout) namespace xvc { -ws_client::ws_client( - std::string_view host, std::string_view port, - std::function event_handler -) +ws_client::ws_client(std::string host, std::string port, std::function handler) { // Launch the asynchronous operation - auto _session = std::make_shared( - host, port, _ioc, [handler = std::move(event_handler)](std::string_view event) { - handler(event); - } - ); + auto _session = + std::make_shared(std::move(host), std::move(port), _ioc, std::move(handler)); _session->run(); _thread = std::jthread([&]() { diff --git a/xdaqvc/ws_client.h b/xdaqvc/ws_client.h index 25a59a4..c551116 100644 --- a/xdaqvc/ws_client.h +++ b/xdaqvc/ws_client.h @@ -1,7 +1,5 @@ #pragma once -#define _WIN32_WINNT 0x0601 - #include #include #include @@ -9,14 +7,12 @@ #include #include #include -#include #include namespace beast = boost::beast; // from namespace websocket = beast::websocket; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from -using namespace std::chrono_literals; // Sends a WebSocket message and prints the response class session : public std::enable_shared_from_this @@ -26,13 +22,13 @@ class session : public std::enable_shared_from_this beast::flat_buffer _buffer; std::string _host; std::string _port; - std::function _handler; + std::function _handler; public: // Resolver and socket require an io_context explicit session( - std::string_view host, std::string_view port, net::io_context &ioc, - std::function event_handler + std::string host, std::string port, net::io_context &ioc, + std::function handler ); // Start the asynchronous operation @@ -47,7 +43,7 @@ class session : public std::enable_shared_from_this void close(); void on_close(beast::error_code ec); - void reconnect(const std::chrono::milliseconds timeout = 500ms); + void reconnect(std::chrono::milliseconds timeout = std::chrono::milliseconds(500)); }; namespace xvc @@ -57,8 +53,8 @@ class ws_client { public: ws_client( - std::string_view host = "192.168.177.100", std::string_view port = "8000", - std::function event_handler = nullptr + std::string host = "192.168.177.100", std::string port = "8000", + std::function handler = nullptr ); ~ws_client(); diff --git a/xdaqvc/xvc.cc b/xdaqvc/xvc.cc index 649978b..ff05cfd 100644 --- a/xdaqvc/xvc.cc +++ b/xdaqvc/xvc.cc @@ -1,39 +1,17 @@ #include "xvc.h" -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include -#include #include -#include #include #include "xdaqmetadata/key_value_store.h" #include "xdaqmetadata/xdaqmetadata.h" using namespace std::chrono_literals; +namespace fs = std::filesystem; namespace { @@ -53,23 +31,17 @@ struct FileTracker { int max_files; }; -gchararray generate_filename( - [[maybe_unused]] GstElement *splitmux, [[maybe_unused]] guint fragment_id, gpointer udata -) +gchararray generate_filename(GstElement *, guint, gpointer udata) { auto tracker = static_cast(udata); - auto now = std::chrono::system_clock::now(); - auto time_t_now = std::chrono::system_clock::to_time_t(now); - std::tm tm_now; - -#ifdef _WIN32 - localtime_s(&tm_now, &time_t_now); -#else - localtime_r(&time_t_now, &tm_now); -#endif + if (!tracker) { + spdlog::error("FileTracker is null"); + return nullptr; + } - auto timestamp = fmt::format("{:%Y-%m-%d_%H-%M-%S}", tm_now); - auto file_path = fmt::format("{}-{}.mkv", tracker->base_filepath, timestamp); + const auto &now = std::chrono::floor(std::chrono::system_clock::now()); + const auto ×tamp = std::format("{:%Y-%m-%d_%H-%M-%S}", now); + const auto &file_path = std::format("{}-{}.mkv", tracker->base_filepath, timestamp); tracker->file_paths.emplace_back(file_path); @@ -146,7 +118,7 @@ void setup_h265_srt_stream(GstPipeline *pipeline, const std::string &uri) ); // clang-format on - g_object_set(G_OBJECT(src), "uri", fmt::format("srt://{}", uri).c_str(), nullptr); + g_object_set(G_OBJECT(src), "uri", std::format("srt://{}", uri).c_str(), nullptr); g_object_set(G_OBJECT(cf_parser), "caps", cf_parser_caps.get(), nullptr); g_object_set(G_OBJECT(cf_dec), "caps", cf_dec_caps.get(), nullptr); g_object_set(G_OBJECT(cf_conv), "caps", cf_conv_caps.get(), nullptr); @@ -207,7 +179,7 @@ void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri) ); // clang-format on - g_object_set(G_OBJECT(src), "uri", fmt::format("srt://{}", uri).c_str(), nullptr); + g_object_set(G_OBJECT(src), "uri", std::format("srt://{}", uri).c_str(), nullptr); g_object_set(G_OBJECT(cf_conv), "caps", cf_conv_caps.get(), nullptr); g_object_set(G_OBJECT(appsink), "sync", false, nullptr); g_object_set(G_OBJECT(fpsdisplaysink), "video-sink", appsink, nullptr); @@ -373,32 +345,17 @@ void stop_h265_recording(GstPipeline *pipeline) ); } -void start_jpeg_recording( - GstPipeline *pipeline, fs::path &filepath, bool split, int max_size_time, TimeUnit unit, - bool loop, int max_files -) +bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) { if (!pipeline) { spdlog::error("Pipeline is null"); - return; + return false; } - if (filepath.empty()) { + if (config._path.empty()) { spdlog::error("filepath is empty"); - return; + return false; } - - auto path = filepath.parent_path(); - if (!fs::exists(path)) { - spdlog::info("Create Directory: {}", path.generic_string()); - std::error_code ec; - if (!fs::create_directories(path, ec)) { - spdlog::info( - "Failed to create directory: {}. Error: {}", path.generic_string(), ec.message() - ); - } - } - - spdlog::info("Start M-JPEG recording"); + spdlog::info("Starting M-JPEG recording ..."); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); if (auto exist_tee_srcpad = gst_element_get_static_pad(tee, "src_1")) { @@ -413,19 +370,11 @@ void start_jpeg_recording( auto muxer = create_element("matroskamux", "muxer"); auto filesink = create_element("splitmuxsink", "filesink"); - switch (unit) { - case TimeUnit::Minutes: max_size_time = max_size_time * 60; break; - case TimeUnit::Hours: max_size_time = max_size_time * 60 * 60; break; - case TimeUnit::Days: max_size_time = max_size_time * 60 * 60 * 24; break; - default: break; - } - - max_files = loop ? max_files : INT_MAX; - - auto tracker = - std::make_unique(FileTracker{filepath.generic_string(), {}, max_files}); - - g_signal_connect(filesink, "format-location", G_CALLBACK(generate_filename), tracker.release()); + auto tracker = new FileTracker(config._path.generic_string(), {}, INT_MAX); + g_object_set_data_full(G_OBJECT(filesink), "file-tracker", tracker, [](gpointer data) { + delete static_cast(data); + }); + g_signal_connect(filesink, "format-location", G_CALLBACK(generate_filename), tracker); // clang-format off g_object_set( @@ -436,7 +385,7 @@ void start_jpeg_recording( ); g_object_set( G_OBJECT(filesink), - "max-size-time", split ? max_size_time * GST_SECOND : 0, // max-size-time=0 -> continuous + "max-size-time", config._split ? config._max_size_time.count() * GST_SECOND : 0, // max-size-time=0 -> continuous "async-finalize", false, "muxer", muxer, nullptr @@ -446,8 +395,8 @@ void start_jpeg_recording( gst_bin_add_many(GST_BIN(pipeline), queue, parser, filesink, nullptr); if (!gst_element_link_many(queue, parser, filesink, nullptr)) { - spdlog::error("Elements could not be linked."); - return; + spdlog::error("Elements could not be linked"); + return false; } gst_element_sync_state_with_parent(queue); @@ -457,23 +406,35 @@ void start_jpeg_recording( auto queue_sinkpad = gst_element_get_static_pad(queue, "sink"); if (gst_pad_link(tee_srcpad, queue_sinkpad) != GST_PAD_LINK_OK) { spdlog::error("Failed to link 'tee' srcpad to 'queue' sinkpad"); + return false; } gst_object_unref(queue_sinkpad); gst_object_unref(tee); GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "after-link"); + return true; } -void stop_jpeg_recording(GstPipeline *pipeline) +bool stop_jpeg_recording(GstPipeline *pipeline) { if (!pipeline) { spdlog::error("Pipeline is null"); - return; + return false; } - spdlog::info("Stop M-JPEG recording"); + spdlog::info("Stopping M-JPEG recording ..."); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); + if (!tee) { + spdlog::error("Failed to get 'tee' element from pipeline"); + return false; + } + auto tee_srcpad = gst_element_get_static_pad(tee, "src_1"); + if (!tee_srcpad) { + spdlog::error("Failed to get 'tee' src_1 pad"); + gst_object_unref(tee); + return false; + } gst_pad_add_probe( tee_srcpad, @@ -481,7 +442,7 @@ void stop_jpeg_recording(GstPipeline *pipeline) [](GstPad *tee_srcpad, [[maybe_unused]] GstPadProbeInfo *info, gpointer user_data) -> GstPadProbeReturn { - spdlog::info("Unlinking"); + spdlog::debug("Unlinking"); auto pipeline = GST_PIPELINE(user_data); auto queue = gst_bin_get_by_name(GST_BIN(pipeline), "queue_record"); @@ -498,98 +459,10 @@ void stop_jpeg_recording(GstPipeline *pipeline) pipeline, nullptr ); -} - -void mock_camera( - GstPipeline *pipeline, [[maybe_unused]] const std::string &uri, const std::string ¤t_cap -) -{ - spdlog::info("Setup GStreamer mock camera SRT Stream"); - - auto parser = [](const std::string &camera_cap) { - std::unordered_map caps_map; - std::stringstream ss(camera_cap); - std::string token; - - // "image/jpeg,width=640,height=480,framerate=601/1"; - while (std::getline(ss, token, ',')) { - auto pos = token.find('='); - if (pos != std::string::npos) { - auto k = token.substr(0, pos); - auto v = token.substr(pos + 1); - caps_map[k] = v; - } else { - caps_map["media_type"] = token; - } - } - return caps_map; - }; - std::unordered_map caps_map = parser(current_cap); - - // auto media_type = caps_map["media_type"]; - auto width = std::stoi(caps_map["width"]); - auto height = std::stoi(caps_map["height"]); - - std::stringstream ss(caps_map["framerate"]); - auto fps_n = 0, fps_d = 1; - char slash; - ss >> fps_n >> slash >> fps_d; - - auto src = create_element("videotestsrc", "src"); - auto cf_src = create_element("capsfilter", "cf_src"); - auto tee = create_element("tee", "t"); - auto queue_display = create_element("queue", "queue_display"); - - auto enc = create_element("jpegenc", "enc"); - // auto dec = create_element("jpegdec", "dec"); - auto fpsdisplaysink = create_element("fpsdisplaysink", "fpsdisplaysink"); - auto appsink = create_element("appsink", "appsink"); - - // clang-format off - std::unique_ptr cf_src_caps( - gst_caps_new_simple( - "video/x-raw", - "format", G_TYPE_STRING, "RGB", - "width", G_TYPE_INT, width, - "height", G_TYPE_INT, height, - "framerate", GST_TYPE_FRACTION, fps_n, fps_d, - nullptr - ), - gst_caps_unref - ); - // clang-format on - - g_object_set(G_OBJECT(src), "pattern", 18, nullptr); - g_object_set(G_OBJECT(src), "is-live", true, nullptr); - g_object_set(G_OBJECT(cf_src), "caps", cf_src_caps.get(), nullptr); - g_object_set(G_OBJECT(fpsdisplaysink), "video-sink", appsink, nullptr); - g_object_set(G_OBJECT(fpsdisplaysink), "sync", false, nullptr); - g_object_set(G_OBJECT(fpsdisplaysink), "text-overlay", false, nullptr); - g_object_set(G_OBJECT(appsink), "sync", false, nullptr); - gst_bin_add_many( - GST_BIN(pipeline), src, cf_src, enc, tee, queue_display, fpsdisplaysink, nullptr - ); - - if (!gst_element_link_many(src, cf_src, enc, tee, queue_display, fpsdisplaysink, nullptr)) { - spdlog::error("Elements could not be linked."); - gst_object_unref(pipeline); - } - - // FPS watcher thread - // auto pipeline_name = gst_element_get_name(GST_ELEMENT(pipeline)); - - // std::thread([pipeline_name, fpsdisplaysink]() { - // while (true) { - // gchar *msg = nullptr; - // g_object_get(G_OBJECT(fpsdisplaysink), "last-message", &msg, nullptr); - // if (msg) { - // spdlog::info("fps{}: {}", pipeline_name, msg); - // g_free(msg); - // } - // std::this_thread::sleep_for(std::chrono::seconds(1)); - // } - // }).detach(); + gst_object_unref(tee); + gst_object_unref(tee_srcpad); + return true; } void parse_video_save_binary_h265(const std::string &video_filepath) @@ -603,7 +476,7 @@ void parse_video_save_binary_h265(const std::string &video_filepath) bin_store.openFile(); - auto pipeline_str = fmt::format( + auto pipeline_str = std::format( "filesrc location=\"{}\" ! matroskademux ! h265parse name=h265parse ! video/x-h265, " "stream-format=byte-stream, alignment=au ! fakesink", video_filepath @@ -689,7 +562,7 @@ void parse_video_save_binary_jpeg(const std::string &video_filepath) bin_store.openFile(); - auto pipeline_str = fmt::format( + auto pipeline_str = std::format( "filesrc location=\"{}\" ! matroskademux ! jpegparse name=jpegparse ! fakesink", video_filepath ); diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index 3e75ecf..3ec3994 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -1,36 +1,39 @@ #pragma once -#define LIBXVC_API_VER "0.2.1" - #include +#include #include #include -namespace fs = std::filesystem; - namespace xvc { -enum class TimeUnit { Seconds = 0, Minutes, Hours, Days }; +struct RecordConfig { + std::filesystem::path _path; + bool _split; + std::chrono::seconds _max_size_time; + + RecordConfig( + std::filesystem::path path = std::filesystem::current_path(), bool split = false, + std::chrono::seconds max_size_time = std::chrono::seconds(10) + ) + : _path(std::move(path)), _split(split), _max_size_time(max_size_time) + { + } +}; void setup_h265_srt_stream(GstPipeline *pipeline, const std::string &uri); void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri); -void mock_camera( - GstPipeline *pipeline, [[maybe_unused]] const std::string &uri, const std::string ¤t_cap -); - void start_h265_recording( - GstPipeline *pipeline, fs::path &filepath, bool continuous, int max_size_time, int max_files + GstPipeline *pipeline, std::filesystem::path &filepath, bool continuous, int max_size_time, + int max_files ); void stop_h265_recording(GstPipeline *pipeline); -void start_jpeg_recording( - GstPipeline *pipeline, fs::path &filepath, bool continuous = true, int max_size_time = 10, - TimeUnit unit = TimeUnit::Minutes, bool loop = false, int max_files = 10 -); -void stop_jpeg_recording(GstPipeline *pipeline); +bool start_jpeg_recording(GstPipeline *, const RecordConfig &); +bool stop_jpeg_recording(GstPipeline *); void parse_video_save_binary_h265(const std::string &filepath); void parse_video_save_binary_jpeg(const std::string &filepath); From 712349c8ae9837ee7f6b743fba031c6c7de474f6 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 11 Feb 2026 17:35:09 +0800 Subject: [PATCH 26/36] Bump version to 0.3.0 --- CMakeLists.txt | 2 +- conanfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 713c2f0..7de1256 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -set(libxvc_VERSION 0.2.1) +set(libxvc_VERSION 0.3.0) project(libxvc LANGUAGES CXX diff --git a/conanfile.py b/conanfile.py index f619d1b..2bfcbde 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,7 +4,7 @@ class libxvc(ConanFile): name = "libxvc" - version = "0.2.1" + version = "0.3.0" settings = "os", "compiler", "build_type", "arch" generators = "VirtualRunEnv" license = "LGPL-3.0-or-later" From 0c3d5ee47f4ac96d67e451f9af7c60c44d561f63 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 11 Feb 2026 17:38:37 +0800 Subject: [PATCH 27/36] fix: upgrade xdaqmetadata version --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 2bfcbde..f9f3444 100644 --- a/conanfile.py +++ b/conanfile.py @@ -27,7 +27,7 @@ def requirements(self): self.requires("nlohmann_json/3.11.3") self.requires("json-schema-validator/2.3.0") self.requires("cpr/1.10.5") - self.requires("xdaqmetadata/0.1.1") + self.requires("xdaqmetadata/0.2.0") self.requires("openssl/3.4.1") self.requires("cli11/2.5.0") From d48671d11df8a611911bc14ea804571f67a31c91 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 11 Feb 2026 18:00:10 +0800 Subject: [PATCH 28/36] fix: not expose json schema validator library --- xdaqvc/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 39b5c8b..10b2e11 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -56,10 +56,11 @@ target_link_libraries(xvc cpr::cpr spdlog::spdlog nlohmann_json::nlohmann_json - nlohmann_json_schema_validator PkgConfig::gstreamer xdaqmetadata::xdaqmetadata Boost::boost + PRIVATE + nlohmann_json_schema_validator ) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) From 38c43282272e12a616bd087c1f739a0f2c5259a7 Mon Sep 17 00:00:00 2001 From: henry Date: Tue, 21 Apr 2026 14:59:58 +0800 Subject: [PATCH 29/36] add: h265 media type is now available --- xdaqvc/camera.cc | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index b52f3d4..b51095c 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -176,9 +176,45 @@ bool Camera::start(const Cap &cap, std::chrono::milliseconds duration) { const nlohmann::json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; - std::string _url; - if (cap.media_type == "image/jpeg") { - _url = url(MJPEG); + auto camera = new Camera(id, name); + // auto camera = std::make_unique( + // camera_json["id"].get(), camera_json["name"].get() + // ); + + for (const auto &cap_json : caps_json) { + Camera::Cap cap{ + .media_type = cap_json.at("media_type").get(), + .format = cap_json.at("format").get(), + .width = cap_json.at("width").get(), + .height = cap_json.at("height").get() + }; + + auto framerate_str = cap_json.at("framerate").get(); + auto delimiter_pos = framerate_str.find('/'); + if (delimiter_pos != std::string::npos) { + cap.fps_n = std::stoi(framerate_str.substr(0, delimiter_pos)); + cap.fps_d = std::stoi(framerate_str.substr(delimiter_pos + 1)); + } + + if (cap.media_type != "image/jpeg" && cap.media_type != "video/x-h265") { + continue; + } + camera->add_cap(cap); + } + + return camera; +} + +void Camera::start(const Cap &cap, const std::chrono::milliseconds duration) +{ + const json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; + + std::string_view url; + + if (_test) { + url = Test; + } else if (cap.media_type == "image/jpeg") { + url = MJPEG; } else if (cap.media_type == "video/x-h265") { _url = url(H265); } else if (cap.media_type == "video/x-h264") { From 259f84bf2fa0e000d365490b5f9d89f4b9567275 Mon Sep 17 00:00:00 2001 From: henry Date: Wed, 25 Mar 2026 18:11:59 +0800 Subject: [PATCH 30/36] Update: - dependency on xdaqmetadata version to 0.1.2 - export_release_debug.bat for building Debug and Release packages. - MSVC not use ASAN option --- conanfile.py | 3 ++- export_relase_debug.bat | 16 ++++++++++++++++ xdaqvc/CMakeLists.txt | 10 ++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 export_relase_debug.bat diff --git a/conanfile.py b/conanfile.py index f9f3444..256cfd3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -12,6 +12,7 @@ class libxvc(ConanFile): description = "Thor Vision Video Capture library" options = {"build_testing": [True, False]} default_options = {"build_testing": False} + exports_sources = "CMakeLists.txt", "cmake/*", "xdaqvc/*", "tool/*", "test/*" def build_requirements(self): self.tool_requires("cmake/[>=3.25.0 <3.30.0]") @@ -27,7 +28,7 @@ def requirements(self): self.requires("nlohmann_json/3.11.3") self.requires("json-schema-validator/2.3.0") self.requires("cpr/1.10.5") - self.requires("xdaqmetadata/0.2.0") + self.requires("xdaqmetadata/0.1.2") self.requires("openssl/3.4.1") self.requires("cli11/2.5.0") diff --git a/export_relase_debug.bat b/export_relase_debug.bat new file mode 100644 index 0000000..44977d2 --- /dev/null +++ b/export_relase_debug.bat @@ -0,0 +1,16 @@ +@REM Remove existing package from cache to ensure a fresh build +call conan remove -c "libxvc/*" + +@REM Build and create Debug package +@REM add options to build depended library: --build=spdlog* --build=fmt* +call conan create . -pr:a default -s build_type=Debug -s compiler.runtime_type=Debug -b missing + +@REM Build and create Release package +@REM add options to build depended library: --build=spdlog* --build=fmt* +call conan create . -pr:a default -s build_type=Release -s compiler.runtime_type=Release -b missing + +echo. +echo Verifying packages in Conan cache: +call conan list libxvc/0.1.2:* +echo. +echo Build complete! Both Debug and Release packages are now in the Conan cache. \ No newline at end of file diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 10b2e11..3d6b03b 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -45,10 +45,12 @@ target_compile_options(xvc ) if(CMAKE_BUILD_TYPE MATCHES "Debug") - target_compile_options(xvc PUBLIC -fsanitize=address,undefined) - target_link_options(xvc PUBLIC -fsanitize=address,undefined) - # target_compile_options(xvc PUBLIC -fsanitize=thread) - # target_link_options(xvc PUBLIC -fsanitize=thread) + if(MSVC) + message(STATUS "MSVC Debug not set ASAN options") + else() + target_compile_options(libxvc PUBLIC -fsanitize=address,undefined) + target_link_options(libxvc PUBLIC -fsanitize=address,undefined) + endif() endif() target_link_libraries(xvc From 57ac39a4f3c00f517f371a444996e558d04a0ec9 Mon Sep 17 00:00:00 2001 From: henry Date: Thu, 26 Mar 2026 18:47:36 +0800 Subject: [PATCH 31/36] Add: export_release_debug.bat script --- scripts/export_relase_debug.bat | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scripts/export_relase_debug.bat diff --git a/scripts/export_relase_debug.bat b/scripts/export_relase_debug.bat new file mode 100644 index 0000000..44977d2 --- /dev/null +++ b/scripts/export_relase_debug.bat @@ -0,0 +1,16 @@ +@REM Remove existing package from cache to ensure a fresh build +call conan remove -c "libxvc/*" + +@REM Build and create Debug package +@REM add options to build depended library: --build=spdlog* --build=fmt* +call conan create . -pr:a default -s build_type=Debug -s compiler.runtime_type=Debug -b missing + +@REM Build and create Release package +@REM add options to build depended library: --build=spdlog* --build=fmt* +call conan create . -pr:a default -s build_type=Release -s compiler.runtime_type=Release -b missing + +echo. +echo Verifying packages in Conan cache: +call conan list libxvc/0.1.2:* +echo. +echo Build complete! Both Debug and Release packages are now in the Conan cache. \ No newline at end of file From a2eab600dc4825ecf700afba55d6a1543bc06adc Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 31 Mar 2026 18:09:01 +0800 Subject: [PATCH 32/36] fix: typo on script name --- export_relase_debug.bat => export_release_debug.bat | 0 scripts/{export_relase_debug.bat => export_release_debug.bat} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename export_relase_debug.bat => export_release_debug.bat (100%) rename scripts/{export_relase_debug.bat => export_release_debug.bat} (100%) diff --git a/export_relase_debug.bat b/export_release_debug.bat similarity index 100% rename from export_relase_debug.bat rename to export_release_debug.bat diff --git a/scripts/export_relase_debug.bat b/scripts/export_release_debug.bat similarity index 100% rename from scripts/export_relase_debug.bat rename to scripts/export_release_debug.bat From c5db897aa1002be02ed094ea02ecc8632fd3888f Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 11 Feb 2026 17:33:05 +0800 Subject: [PATCH 33/36] Update start_jpeg_recording API Hide third-party library from headers Upgrade cpp standard to 23 Add tests for camera & port_pool & ws_client Add json schema parsing camera Update CLI Seperate updater and xvc depedencies --- CMakeLists.txt | 2 +- conanfile.py | 3 +-- xdaqvc/CMakeLists.txt | 3 +-- xdaqvc/camera.cc | 46 ++++--------------------------------------- 4 files changed, 7 insertions(+), 47 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7de1256..713c2f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -set(libxvc_VERSION 0.3.0) +set(libxvc_VERSION 0.2.1) project(libxvc LANGUAGES CXX diff --git a/conanfile.py b/conanfile.py index 256cfd3..07d4bb1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -12,7 +12,7 @@ class libxvc(ConanFile): description = "Thor Vision Video Capture library" options = {"build_testing": [True, False]} default_options = {"build_testing": False} - exports_sources = "CMakeLists.txt", "cmake/*", "xdaqvc/*", "tool/*", "test/*" + # exports_sources = "CMakeLists.txt", "cmake/*", "xdaqvc/*", "tool/*", "test/*" def build_requirements(self): self.tool_requires("cmake/[>=3.25.0 <3.30.0]") @@ -23,7 +23,6 @@ def build_requirements(self): def requirements(self): self.requires("boost/1.81.0") - self.requires("fmt/10.2.1") self.requires("spdlog/1.13.0") self.requires("nlohmann_json/3.11.3") self.requires("json-schema-validator/2.3.0") diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 3d6b03b..0273fa8 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -58,11 +58,10 @@ target_link_libraries(xvc cpr::cpr spdlog::spdlog nlohmann_json::nlohmann_json + nlohmann_json_schema_validator PkgConfig::gstreamer xdaqmetadata::xdaqmetadata Boost::boost - PRIVATE - nlohmann_json_schema_validator ) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index b51095c..b63980b 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -138,7 +138,7 @@ std::unique_ptr Camera::parse(std::string_view camera_json) cap.fps_d = std::stoi(framerate.substr(slash + 1)); } - if (cap.media_type == "image/jpeg") { + if (cap.media_type == "image/jpeg" || cap.media_type == "video/x-h265") { camera->add_cap(cap); } } @@ -176,49 +176,11 @@ bool Camera::start(const Cap &cap, std::chrono::milliseconds duration) { const nlohmann::json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; - auto camera = new Camera(id, name); - // auto camera = std::make_unique( - // camera_json["id"].get(), camera_json["name"].get() - // ); - - for (const auto &cap_json : caps_json) { - Camera::Cap cap{ - .media_type = cap_json.at("media_type").get(), - .format = cap_json.at("format").get(), - .width = cap_json.at("width").get(), - .height = cap_json.at("height").get() - }; - - auto framerate_str = cap_json.at("framerate").get(); - auto delimiter_pos = framerate_str.find('/'); - if (delimiter_pos != std::string::npos) { - cap.fps_n = std::stoi(framerate_str.substr(0, delimiter_pos)); - cap.fps_d = std::stoi(framerate_str.substr(delimiter_pos + 1)); - } - - if (cap.media_type != "image/jpeg" && cap.media_type != "video/x-h265") { - continue; - } - camera->add_cap(cap); - } - - return camera; -} - -void Camera::start(const Cap &cap, const std::chrono::milliseconds duration) -{ - const json payload{{"id", _id}, {"capability", cap.to_string()}, {"port", _port}}; - - std::string_view url; - - if (_test) { - url = Test; - } else if (cap.media_type == "image/jpeg") { - url = MJPEG; + std::string _url; + if (cap.media_type == "image/jpeg") { + _url = url(MJPEG); } else if (cap.media_type == "video/x-h265") { _url = url(H265); - } else if (cap.media_type == "video/x-h264") { - _url = url(H264); } else { spdlog::error("Unsupported codec for camera id: {}", _id); return false; From 67e51d8ac649d0df64183ee7b9b2bb13cd98f1e7 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Wed, 11 Feb 2026 17:38:37 +0800 Subject: [PATCH 34/36] fix: upgrade xdaqmetadata version --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 07d4bb1..5bcbc17 100644 --- a/conanfile.py +++ b/conanfile.py @@ -27,7 +27,7 @@ def requirements(self): self.requires("nlohmann_json/3.11.3") self.requires("json-schema-validator/2.3.0") self.requires("cpr/1.10.5") - self.requires("xdaqmetadata/0.1.2") + self.requires("xdaqmetadata/0.2.0") self.requires("openssl/3.4.1") self.requires("cli11/2.5.0") From 422f309f1d29c114be3d7293ca21285a76b71262 Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 12 Mar 2026 15:03:14 +0800 Subject: [PATCH 35/36] minor library change --- test/CMakeLists.txt | 2 ++ test/test_ws_client.cc | 2 -- tool/CMakeLists.txt | 1 + xdaqvc/CMakeLists.txt | 8 +++++--- xdaqvc/server.cc | 6 +++--- xdaqvc/xvc.cc | 10 ++++++++-- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 60bdde2..e8a0ca8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(xvc_updater_tests target_link_libraries(xvc_updater_tests PRIVATE updater + fmt::fmt gtest::gtest ) target_include_directories(xvc_updater_tests @@ -43,6 +44,7 @@ add_executable(test_ws_client test_ws_client.cc) target_link_libraries(test_ws_client PRIVATE xvc + nlohmann_json::nlohmann_json Catch2::Catch2WithMain ) target_compile_features(test_ws_client PRIVATE cxx_std_23) diff --git a/test/test_ws_client.cc b/test/test_ws_client.cc index b4ba026..5085e11 100644 --- a/test/test_ws_client.cc +++ b/test/test_ws_client.cc @@ -1,5 +1,3 @@ -#include - #include #include diff --git a/tool/CMakeLists.txt b/tool/CMakeLists.txt index a866b14..305ba2a 100644 --- a/tool/CMakeLists.txt +++ b/tool/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(xvc_update_tool PRIVATE updater CLI11::CLI11 + spdlog::spdlog ) target_compile_features(xvc_update_tool PRIVATE cxx_std_23) target_compile_options(xvc_update_tool diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 0273fa8..7f5c1b8 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -55,13 +55,15 @@ endif() target_link_libraries(xvc PUBLIC + Boost::boost + # TODO: + xdaqmetadata::xdaqmetadata + PRIVATE cpr::cpr spdlog::spdlog nlohmann_json::nlohmann_json nlohmann_json_schema_validator PkgConfig::gstreamer - xdaqmetadata::xdaqmetadata - Boost::boost ) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) @@ -105,7 +107,7 @@ target_compile_options(updater $<$>:-Wall> ) target_link_libraries(updater - PUBLIC + PRIVATE cpr::cpr spdlog::spdlog nlohmann_json::nlohmann_json diff --git a/xdaqvc/server.cc b/xdaqvc/server.cc index ed3b70b..26445d8 100644 --- a/xdaqvc/server.cc +++ b/xdaqvc/server.cc @@ -23,7 +23,7 @@ bool Server::root(std::chrono::milliseconds timeout) const { auto response = cpr::Get(cpr::Url{_base_url}, cpr::Timeout{timeout}); if (response.status_code != OK) { - spdlog::error("Failed to fetch {}. Status: {}", _base_url, response.status_code); + spdlog::debug("Failed to fetch {} Status: {}", _base_url, response.status_code); return false; } return true; @@ -39,7 +39,7 @@ std::optional Server::logs( auto response = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); if (response.status_code != OK) { - spdlog::error("Failed to fetch {}. Status: {}", url, response.status_code); + spdlog::debug("Failed to fetch {} Status: {}", url, response.status_code); return std::nullopt; } return logs ? nlohmann::json::parse(response.text).dump(2) : response.text; @@ -51,7 +51,7 @@ std::optional Server::api_version(std::chrono::milliseconds timeout) co auto response = cpr::Get(cpr::Url{url}, cpr::Timeout{timeout}); if (response.status_code != OK) { - spdlog::error("Failed to fetch {}. Status: {}", url, response.status_code); + spdlog::debug("Failed to fetch {} Status: {}", url, response.status_code); return std::nullopt; } diff --git a/xdaqvc/xvc.cc b/xdaqvc/xvc.cc index ff05cfd..8b3c9bd 100644 --- a/xdaqvc/xvc.cc +++ b/xdaqvc/xvc.cc @@ -357,6 +357,12 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) } spdlog::info("Starting M-JPEG recording ..."); + const auto &location = config._path.generic_string(); + const auto split = config._split; + const auto max_size_time = config._max_size_time; + + spdlog::info("max_size_time = {}", max_size_time.count()); + auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); if (auto exist_tee_srcpad = gst_element_get_static_pad(tee, "src_1")) { spdlog::warn("tee 'src_1' pad already exists, releasing it..."); @@ -370,7 +376,7 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) auto muxer = create_element("matroskamux", "muxer"); auto filesink = create_element("splitmuxsink", "filesink"); - auto tracker = new FileTracker(config._path.generic_string(), {}, INT_MAX); + auto tracker = new FileTracker(location, {}, INT_MAX); g_object_set_data_full(G_OBJECT(filesink), "file-tracker", tracker, [](gpointer data) { delete static_cast(data); }); @@ -385,7 +391,7 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) ); g_object_set( G_OBJECT(filesink), - "max-size-time", config._split ? config._max_size_time.count() * GST_SECOND : 0, // max-size-time=0 -> continuous + "max-size-time", split ? max_size_time.count() * GST_SECOND : 0, // max-size-time=0 -> continuous "async-finalize", false, "muxer", muxer, nullptr From 72314ca6251c67eb81529f612206ef1942cdc46d Mon Sep 17 00:00:00 2001 From: Steven Shen Date: Thu, 7 May 2026 17:33:37 +0800 Subject: [PATCH 36/36] Add cmake options to use ASAN/UBSAN/TSAN Remove conan option bypassing to cmake to build test, and remove gtest Update cpr library from 1.10.5 to 1.14.2 Remove script to export local conan cache Remove script to benchmark qt's decode max capability Remove Updater related code and deps, as it is in another branch Remove H.265/MJPEG parse saving function, as right now it is unused --- .gitignore | 14 +- CMakeLists.txt | 136 ++++++- conanfile.py | 12 +- export_release_debug.bat | 16 - scripts/export_release_debug.bat | 16 - scripts/plot.py | 103 ----- scripts/test_decode.py | 220 ----------- test/updater_test.cc | 152 -------- test/updater_test_base.h | 57 --- {test => tests}/CMakeLists.txt | 29 -- {test => tests}/test_camera.cc | 0 {test => tests}/test_port_pool.cc | 0 {test => tests}/test_ws_client.cc | 0 tool/CMakeLists.txt | 28 -- tool/xvc_update_tool.cc | 113 ------ tools/CMakeLists.txt | 15 + {tool => tools}/tvcli.cc | 15 +- xdaqvc/CMakeLists.txt | 122 ++---- xdaqvc/camera.cc | 1 - xdaqvc/camera.h | 4 +- xdaqvc/server.h | 6 +- xdaqvc/updater.cc | 622 ------------------------------ xdaqvc/updater.h | 93 ----- xdaqvc/xvc.cc | 511 +++--------------------- xdaqvc/xvc.h | 11 +- 25 files changed, 250 insertions(+), 2046 deletions(-) delete mode 100644 export_release_debug.bat delete mode 100644 scripts/export_release_debug.bat delete mode 100644 scripts/plot.py delete mode 100644 scripts/test_decode.py delete mode 100644 test/updater_test.cc delete mode 100644 test/updater_test_base.h rename {test => tests}/CMakeLists.txt (60%) rename {test => tests}/test_camera.cc (100%) rename {test => tests}/test_port_pool.cc (100%) rename {test => tests}/test_ws_client.cc (100%) delete mode 100644 tool/CMakeLists.txt delete mode 100644 tool/xvc_update_tool.cc create mode 100644 tools/CMakeLists.txt rename {tool => tools}/tvcli.cc (96%) delete mode 100644 xdaqvc/updater.cc delete mode 100644 xdaqvc/updater.h diff --git a/.gitignore b/.gitignore index f6d94aa..19a90ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # IDE, Editor +__pycache__/ +*.py[cod] /*.pro.user* /.vscode @@ -6,13 +8,15 @@ .DS_Store # Build -/build* -/.venv -/.cache -/CMakeUserPresets.json +dist/ +build/ +.venv/ +.cache/ +CMakeUserPresets.json # clang .clangd *.log -*.mkv \ No newline at end of file +*.mkv +*.sh \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 713c2f0..d92b6eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,48 +5,142 @@ set(libxvc_VERSION 0.2.1) project(libxvc LANGUAGES CXX VERSION "${libxvc_VERSION}" - DESCRIPTION "Thor Vision Video Capture Library" - HOMEPAGE_URL "https://github.com/kontex-neuro/libxvc" ) -if(APPLE) - add_compile_options($<$:-fexperimental-library>) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(Boost_USE_STATIC_LIBS ON) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "default install path" FORCE) endif() -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +option(BUILD_SHARED_LIBS "Build as shared library" OFF) +option(BUILD_TESTS "Build tests" OFF) +option(BUILD_TOOLS "Build tools" OFF) +# option(BUILD_PYTHON_BINDINGS "Build python bindings" ON) +# option(BUILD_PYTHON_STUBS "Build python stubs" OFF) +option(USE_ASAN "Enable AddressSanitizer (ASan)" OFF) +option(USE_UBSAN "Enable UndefinedBehaviorSanitizer (UBSan)" OFF) +option(USE_TSAN "Enable ThreadSanitizer (TSan)" OFF) + +if(USE_ASAN AND USE_TSAN) + message( + FATAL_ERROR + "AddressSanitizer (ASan) and ThreadSanitizer (TSan) are mutually exclusive and cannot be enabled at the same time." + ) +endif() + +set(SANITIZER_COMPILE_FLAGS "") +set(SANITIZER_LINK_FLAGS "") + +if(USE_ASAN) + if(WIN32 AND MSVC) + list(APPEND SANITIZER_COMPILE_FLAGS /fsanitize=address) + list(APPEND SANITIZER_LINK_FLAGS /fsanitize=address) + else() + list(APPEND SANITIZER_COMPILE_FLAGS -fsanitize=address) + list(APPEND SANITIZER_LINK_FLAGS -fsanitize=address) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND SANITIZER_LINK_FLAGS -lpthread) + endif() + endif() +endif() + +if(USE_UBSAN) + if(WIN32 AND MSVC) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + list(APPEND SANITIZER_COMPILE_FLAGS -fsanitize=undefined) + list(APPEND SANITIZER_LINK_FLAGS -fsanitize=undefined) + else() + message( + WARNING + "UndefinedBehaviorSanitizer (UBSan) is not directly supported via a simple flag in MSVC." + ) + endif() + else() + list(APPEND SANITIZER_COMPILE_FLAGS -fsanitize=undefined) + list(APPEND SANITIZER_LINK_FLAGS -fsanitize=undefined) + endif() +endif() + +if(USE_TSAN) + if(WIN32 AND MSVC) + message( + FATAL_ERROR + "ThreadSanitizer (TSan) is not supported by the MSVC compiler. Please use Clang or GCC." + ) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + message(FATAL_ERROR "ThreadSanitizer (TSan) is not supported on macOS.") + else() + list(APPEND SANITIZER_COMPILE_FLAGS -fsanitize=thread) + list(APPEND SANITIZER_LINK_FLAGS -fsanitize=thread) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND SANITIZER_LINK_FLAGS -lpthread) + endif() + endif() +endif() + +add_library(xvc) + +target_compile_options(xvc PUBLIC ${SANITIZER_COMPILE_FLAGS}) +target_link_options(xvc PUBLIC ${SANITIZER_LINK_FLAGS}) -set(Boost_USE_STATIC_LIBS ON) -find_package(fmt REQUIRED) find_package(spdlog REQUIRED) find_package(nlohmann_json REQUIRED) find_package(nlohmann_json_schema_validator REQUIRED) find_package(cpr REQUIRED) -find_package(xdaqmetadata REQUIRED) find_package(Boost 1.81.0 REQUIRED) -find_package(CLI11 REQUIRED) -find_package(OpenSSL REQUIRED) - find_package(PkgConfig REQUIRED) pkg_search_module(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0>=1.4) -pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4) -pkg_search_module(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0>=1.4) - -option(BUILD_TESTING "Build tests" OFF) add_subdirectory(xdaqvc) -add_subdirectory(tool) -if(BUILD_TESTING) +if(BUILD_TESTS) enable_testing() - find_package(GTest REQUIRED) find_package(Catch2 REQUIRED) - include(CTest) - add_subdirectory(test) + add_subdirectory(tests) endif() -include(CMakePackageConfigHelpers) +if(BUILD_TOOLS) + find_package(xdaqmetadata REQUIRED) + find_package(CLI11 REQUIRED) + pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4) + pkg_search_module(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0>=1.4) + add_subdirectory(tools) +endif() + +# if(BUILD_PYTHON_BINDINGS) +# find_package(Python 3.9 +# REQUIRED COMPONENTS Interpreter Development.Module +# # OPTIONAL_COMPONENTS Development.SABIModule +# ) +# find_package(nanobind CONFIG REQUIRED) +# add_subdirectory(python/src) +# endif() + include(GNUInstallDirs) +install( + TARGETS xvc + EXPORT libxvc-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + FILE_SET "public_headers" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/xdaqvc +) +install( + EXPORT libxvc-targets + FILE libxvc-targets.cmake + NAMESPACE libxvc:: + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/libxvc" +) +export( + EXPORT libxvc-targets + FILE libxvc-config.cmake + NAMESPACE libxvc:: +) + +include(CMakePackageConfigHelpers) + configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/libxvc-config.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/libxvc-config.cmake" diff --git a/conanfile.py b/conanfile.py index 5bcbc17..8dd389c 100644 --- a/conanfile.py +++ b/conanfile.py @@ -10,25 +10,20 @@ class libxvc(ConanFile): license = "LGPL-3.0-or-later" url = "https://github.com/kontex-neuro/libxvc.git" description = "Thor Vision Video Capture library" - options = {"build_testing": [True, False]} - default_options = {"build_testing": False} - # exports_sources = "CMakeLists.txt", "cmake/*", "xdaqvc/*", "tool/*", "test/*" + exports_sources = "CMakeLists.txt", "cmake/*", "xdaqvc/*", "tools/*" def build_requirements(self): self.tool_requires("cmake/[>=3.25.0 <3.30.0]") self.tool_requires("ninja/[>=1.12.0]") - if self.options.build_testing: - self.test_requires("catch2/3.8.0") - self.test_requires("gtest/1.14.0") + self.test_requires("catch2/3.8.0") def requirements(self): self.requires("boost/1.81.0") self.requires("spdlog/1.13.0") self.requires("nlohmann_json/3.11.3") self.requires("json-schema-validator/2.3.0") - self.requires("cpr/1.10.5") + self.requires("cpr/1.14.2") self.requires("xdaqmetadata/0.2.0") - self.requires("openssl/3.4.1") self.requires("cli11/2.5.0") def configure(self): @@ -83,7 +78,6 @@ def generate(self): deps.generate() tc = CMakeToolchain(self) tc.generator = "Ninja" - tc.variables["BUILD_TESTING"] = self.options.build_testing tc.generate() def build(self): diff --git a/export_release_debug.bat b/export_release_debug.bat deleted file mode 100644 index 44977d2..0000000 --- a/export_release_debug.bat +++ /dev/null @@ -1,16 +0,0 @@ -@REM Remove existing package from cache to ensure a fresh build -call conan remove -c "libxvc/*" - -@REM Build and create Debug package -@REM add options to build depended library: --build=spdlog* --build=fmt* -call conan create . -pr:a default -s build_type=Debug -s compiler.runtime_type=Debug -b missing - -@REM Build and create Release package -@REM add options to build depended library: --build=spdlog* --build=fmt* -call conan create . -pr:a default -s build_type=Release -s compiler.runtime_type=Release -b missing - -echo. -echo Verifying packages in Conan cache: -call conan list libxvc/0.1.2:* -echo. -echo Build complete! Both Debug and Release packages are now in the Conan cache. \ No newline at end of file diff --git a/scripts/export_release_debug.bat b/scripts/export_release_debug.bat deleted file mode 100644 index 44977d2..0000000 --- a/scripts/export_release_debug.bat +++ /dev/null @@ -1,16 +0,0 @@ -@REM Remove existing package from cache to ensure a fresh build -call conan remove -c "libxvc/*" - -@REM Build and create Debug package -@REM add options to build depended library: --build=spdlog* --build=fmt* -call conan create . -pr:a default -s build_type=Debug -s compiler.runtime_type=Debug -b missing - -@REM Build and create Release package -@REM add options to build depended library: --build=spdlog* --build=fmt* -call conan create . -pr:a default -s build_type=Release -s compiler.runtime_type=Release -b missing - -echo. -echo Verifying packages in Conan cache: -call conan list libxvc/0.1.2:* -echo. -echo Build complete! Both Debug and Release packages are now in the Conan cache. \ No newline at end of file diff --git a/scripts/plot.py b/scripts/plot.py deleted file mode 100644 index 83eb800..0000000 --- a/scripts/plot.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import re -import click -import matplotlib.pyplot as plt -from collections import defaultdict - - -FPS_PATTERN = re.compile(r"fps(\d+):.*average:\s*([\d.]+)") - - -def parse_filename(filename): - match = re.match(r"decode_(\d+)_(\d+)_(\d+)\.log", filename) - if match: - width, height, instances = match.groups() - resolution = f"{width}x{height}" - return resolution, int(instances) - return None, None - - -def parse_fps_from_log(filepath): - results = {} - - with open(filepath, "r") as f: - for line in f: - match = FPS_PATTERN.search(line) - if match: - idx, fps = int(match.group(1)), float(match.group(2)) - results[idx] = fps - - return sum(results.values()) / len(results) if results else None - - -def collect_log_data(folder_path): - data = defaultdict(list) - - for filename in os.listdir(folder_path): - if filename.startswith("decode_") and filename.endswith(".log"): - resolution, instances = parse_filename(filename) - if resolution: - filepath = os.path.join(folder_path, filename) - avg_fps = parse_fps_from_log(filepath) - if avg_fps is not None: - data[resolution].append((instances, avg_fps)) - - for resolution in data: - data[resolution].sort() - - return data - - -def plot_fps_data(data, log_scale=True): - plt.figure(figsize=(12, 7)) - - for resolution, values in data.items(): - x = [inst for inst, _ in values] - y = [fps for _, fps in values] - plt.plot(x, y, marker="o", label=resolution) - - for inst, fps in values: - plt.annotate( - f"{inst}x\n{fps:.1f} FPS", - (inst, fps), - textcoords="offset points", - xytext=(0, 5), - ha="center", - fontsize=8, - ) - - for line in [30, 60, 120]: - plt.axhline(y=line, color="gray", linestyle="--", linewidth=1) - plt.text(plt.xlim()[0], line * 1.05, f"{line} FPS", color="gray", fontsize=9) - - plt.xlabel("Decoder Instances") - plt.ylabel("Average FPS (log scale)" if log_scale else "Average FPS") - if log_scale: - plt.yscale("log") - plt.title("Decoder Performance vs Instance Count") - plt.legend() - plt.grid(True, which="both", linestyle="--", linewidth=0.5) - plt.tight_layout() - plt.show() - - -@click.command() -@click.argument("folder", type=click.Path(exists=True, file_okay=False)) -@click.option("--linear", is_flag=True, help="Use linear scale for FPS (default: log).") -def main(folder, linear): - """ - Visualize decoder performance logs in FOLDER. - - \b - FOLDER: Path to the folder containing average FPS log files. - """ - data = collect_log_data(folder) - if not data: - click.echo("No valid log files found.") - return - - plot_fps_data(data, log_scale=not linear) - - -if __name__ == "__main__": - main() diff --git a/scripts/test_decode.py b/scripts/test_decode.py deleted file mode 100644 index eb4066e..0000000 --- a/scripts/test_decode.py +++ /dev/null @@ -1,220 +0,0 @@ -import subprocess -import re -import os -import time -import click - -FPS_PATTERN = re.compile(r"fps(\d+):.*average:\s*([\d.]+)") - -CODEC_MAP = { - "jpeg": {"encoder": "jpegenc", "decoder": "jpegdec"}, - "h264": { - "encoder": "x264enc speed-preset=ultrafast tune=zerolatency", - "decoder": "avdec_h264", - }, - "h265": { - "encoder": "x265enc speed-preset=ultrafast tune=zerolatency", - "decoder": "avdec_h265", - }, -} - - -def get_codec_elements(codec): - if codec not in CODEC_MAP: - raise ValueError(f"Unsupported codec: {codec}") - return CODEC_MAP[codec]["encoder"], CODEC_MAP[codec]["decoder"] - - -def run_decoder_pipeline( - resolution, - instances, - codec, - log_path, - mode, - video_path=None, -): - encoder, decoder = get_codec_elements(codec) - - decode_branches = [ - f"t. ! queue name=dec{i} ! {decoder} ! " - f"fpsdisplaysink name=fps{i} text-overlay=false video-sink=fakesink sync=false" - for i in range(instances) - ] - - if mode == "file": - pipeline = f"filesrc location={video_path} ! " f"tee name=t " + " ".join( - decode_branches - ) - else: - width, height = resolution.lower().split("x") - pipeline = ( - f"videotestsrc is-live=false pattern=1 ! " - f"video/x-raw,width={width},height={height} ! {encoder} ! " - f"tee name=t " + " ".join(decode_branches) - ) - - cmd = f"gst-launch-1.0 -v {pipeline}" - - with open(log_path, "w") as logfile: - process = subprocess.Popen( - cmd, - shell=True, - stdout=logfile, - stderr=subprocess.STDOUT, - text=True, - ) - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - print("Pipeline timeout. Terminating...") - process.terminate() - process.wait() - - -def analyze_fps_log(log_path, instances, expected_fps): - results = {} - - with open(log_path, "r") as log_file: - for line in reversed(log_file.readlines()): - match = FPS_PATTERN.search(line) - if match: - idx, fps = int(match.group(1)), float(match.group(2)) - if idx not in results: - results[idx] = fps - if len(results) == instances: - break - - all_ok = all(results.get(i, 0) >= expected_fps for i in range(instances)) - return all_ok, results - - -def run_resolution_test( - resolution, - expected_fps, - codec, - max_instances, - mode, - video_path, - run_dir, - label="", -): - for instances in range(1, max_instances + 1): - log_name = ( - f"decode_{label}_{instances}.log" - if label - else f"decode_{resolution.replace('x', '_')}_{instances}.log" - ) - log_path = os.path.join(run_dir, log_name) - - run_decoder_pipeline(resolution, instances, codec, log_path, mode, video_path) - all_ok, fps_data = analyze_fps_log(log_path, instances, expected_fps) - - print(f"\n--- {resolution} | {instances} instance(s) ---") - for i in range(instances): - avg_fps = fps_data.get(i) - if avg_fps is not None: - status = "OK" if avg_fps >= expected_fps else "SLOW" - print( - f"Pipeline {i}: average FPS = {avg_fps:.2f} >= {expected_fps:.2f} => {status}" - ) - else: - print(f"Pipeline {i}: No FPS data found") - - if not all_ok: - print( - f"\nMax sustainable instances for {label or resolution} at {expected_fps:.2f} FPS: {instances - 1}" - ) - return instances - 1 - - instances += 1 - - -def auto_scale_test( - resolutions, - expected_fps=30, - codec="jpeg", - max_instances=64, - mode="encode", - video_path=None, -): - - timestamp = time.strftime("%Y%m%d_%H%M%S") - run_dir = f"test_logs_{timestamp}" - os.makedirs(run_dir, exist_ok=True) - - if mode == "file": - print(f"\n--- Testing file input: {video_path} ---") - resolution = resolutions[0] - run_resolution_test( - resolution, - expected_fps, - codec, - max_instances, - mode, - video_path, - run_dir, - label="file", - ) - else: - for resolution in resolutions: - print(f"\n--- Testing resolution: {resolution} ---") - run_resolution_test( - resolution, - expected_fps, - codec, - max_instances, - mode, - video_path, - run_dir, - ) - - -@click.command() -@click.argument("mode", default="encode", type=click.Choice(["encode", "file"])) -@click.argument( - "resolutions", - nargs=-1, - required=False, -) -@click.argument("fps", default=30, type=int) -@click.option( - "-c", - "--codec", - default="jpeg", - type=click.Choice(["jpeg", "h264", "h265"]), - help="Codec to use.", -) -@click.argument("max-instances", default=64, type=int) -@click.option("-p", "--path", default=None, help="Path to video file (file mode only).") -def main(mode, resolutions, fps, codec, max_instances, path): - """ - Benchmark max sustainable decode branches at a given FPS. - - \b - MODE: 'encode' or 'file' - RESOLUTIONS: e.g., 1920x1080 1280x720 (for 'encode' mode) - FPS: Expected average FPS per pipeline - INSTANCES: Maximum number of decode branches to test - """ - - if mode == "file": - if not path: - raise click.UsageError("In 'file' mode, --path is required.") - resolution_list = ["dummy"] - else: - if not resolutions: - raise click.UsageError("In 'encode' mode, --resolutions is required.") - resolution_list = list(resolutions) - - auto_scale_test( - resolution_list, - fps, - codec, - max_instances, - mode, - path, - ) - - -if __name__ == "__main__": - main() diff --git a/test/updater_test.cc b/test/updater_test.cc deleted file mode 100644 index 02812eb..0000000 --- a/test/updater_test.cc +++ /dev/null @@ -1,152 +0,0 @@ -#include "updater_test_base.h" - - -using namespace std::chrono_literals; - - -TEST_F(XVCUpdaterTest, CalculateSHA256) -{ - // Create a test file with known content - auto test_content = "Hello, World!"; - std::ofstream _test_file("test.txt"); - _test_file << test_content; - _test_file.close(); - - auto hash = xvc::calculate_sha256("test.txt"); - ASSERT_TRUE(hash.has_value()); - EXPECT_FALSE(hash->empty()); - - // Known SHA256 hash for "Hello, World!" - auto expected_hash = "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"; - EXPECT_EQ(*hash, expected_hash); - - fs::remove("test.txt"); -} - -TEST_F(XVCUpdaterTest, DownloadAndVerify) -{ - // Get version table first to get valid hash - auto version_table = xvc::get_version_table(version_table_url); - ASSERT_TRUE(version_table.has_value()); - ASSERT_FALSE(version_table->versions.empty()); - - const auto &test_version = version_table->versions.back(); - auto download_url = fmt::format("https://xvc001.sgp1.cdn.digitaloceanspaces.com/{}", test_file); - - auto result = xvc::download_and_verify(download_url, test_version.hash, test_file); - ASSERT_TRUE(result.success) << "Download failed: " << result.error_message; - - // Verify file exists and has content - ASSERT_TRUE(fs::exists(test_file)); - ASSERT_GT(fs::file_size(test_file), 0); -} - -TEST_F(XVCUpdaterTest, DownloadAndVerifyInvalidHash) -{ - auto result = xvc::download_and_verify( - "https://xvc001.sgp1.cdn.digitaloceanspaces.com/xvc-server-0.0.1.tar.xz", - "invalid_hash", - test_file - ); - EXPECT_FALSE(result.success); - EXPECT_FALSE(result.error_message.empty()); - EXPECT_FALSE(fs::exists(test_file)); -} - -TEST_F(XVCUpdaterTest, HandshakeTest) -{ - auto response = xvc::perform_handshake(server_address, update_server_port); - ASSERT_TRUE(response.success) << "Handshake failed: " << response.error_message; - - EXPECT_FALSE(response.token.empty()); - EXPECT_GT(response.expires, std::chrono::system_clock::now()); -} - -TEST_F(XVCUpdaterTest, FileTransferWorkflow) -{ - // First download a test file - auto version_table = xvc::get_version_table(version_table_url); - ASSERT_TRUE(version_table.has_value()); - ASSERT_FALSE(version_table->versions.empty()); - - const auto &test_version = version_table->versions.back(); - auto download_url = fmt::format("https://xvc001.sgp1.cdn.digitaloceanspaces.com/{}", test_file); - - auto download_result = xvc::download_and_verify(download_url, test_version.hash, test_file); - ASSERT_TRUE(download_result.success); - - // Perform handshake - auto handshake = xvc::perform_handshake(server_address, update_server_port); - ASSERT_TRUE(handshake.success); - - // Prepare transfer - std::string transfer_id; - auto file_size = fs::file_size(test_file); - bool prepared = xvc::prepare_file_transfer( - server_address, - update_server_port, - handshake.token, - test_file, - test_version.hash, - file_size, - transfer_id - ); - ASSERT_TRUE(prepared); - EXPECT_FALSE(transfer_id.empty()); - - // Perform transfer - bool transfer_success = xvc::transfer_file( - server_address, - update_server_port, - handshake.token, - test_file, - transfer_id, - [](const xvc::FileTransferProgress &progress) { - EXPECT_GE(progress.progress_percentage, 0.0f); - EXPECT_LE(progress.progress_percentage, 100.0f); - EXPECT_GT(progress.total_bytes, 0ULL); - } - ); - ASSERT_TRUE(transfer_success); -} - -TEST_F(XVCUpdaterTest, GetServerVersion) -{ - auto version = xvc::get_server_version(server_address, server_port); - ASSERT_TRUE(version.has_value()); - EXPECT_GT(*version, xvc::Version({0, 0, 0})); -} - -TEST_F(XVCUpdaterTest, GetVersionTable) -{ - auto table = xvc::get_version_table(version_table_url); - ASSERT_TRUE(table.has_value()); - EXPECT_FALSE(table->versions.empty()); - EXPECT_EQ(table->latest_version, table->versions.front().version); -} - -TEST_F(XVCUpdaterTest, CompleteUpdateWorkflow) -{ - auto result = xvc::update_server( - server_address, - server_port, - update_server_port, - version_table_url, - update_dir, - client_version - ); - - ASSERT_TRUE(result.success) << "Update failed: " << result.error_message; - - if (result.update_needed) { - EXPECT_GT(result.available_version, result.current_version); - - // Verify update file was downloaded - auto expected_filename = - fmt::format("xvc-server-{}.tar.xz", result.available_version.to_string()); - auto update_path = fs::path(update_dir) / expected_filename; - - EXPECT_TRUE(fs::exists(update_path)); - EXPECT_GT(fs::file_size(update_path), 0); - } -} \ No newline at end of file diff --git a/test/updater_test_base.h b/test/updater_test_base.h deleted file mode 100644 index 71b5829..0000000 --- a/test/updater_test_base.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "updater.h" - - -namespace fs = std::filesystem; - - -class XVCUpdaterTest : public testing::Test -{ -protected: - void SetUp() override - { - server_address = "192.168.177.100"; - server_port = 8000; - update_server_port = 8001; - version_table_url = "https://xvc001.sgp1.digitaloceanspaces.com/versions.json"; - update_dir = "test_updates"; - test_file = "xvc-server-0.0.1.tar.xz"; - client_version = xvc::Version{0, 0, 1}; - } - - void TearDown() override - { - fs::remove_all(update_dir); - if (fs::exists(test_file)) { - fs::remove(test_file); - } - } - - std::string read_file_content(const std::string &filepath) - { - std::ifstream file(filepath); - if (!file.is_open()) { - ADD_FAILURE() << "Could not open file: " << filepath; - return std::string(""); - } - return std::string( - (std::istreambuf_iterator(file)), std::istreambuf_iterator() - ); - } - -public: - std::string server_address; - int server_port; - int update_server_port; - std::string version_table_url; - std::string update_dir; - std::string test_file; - xvc::Version client_version; -}; \ No newline at end of file diff --git a/test/CMakeLists.txt b/tests/CMakeLists.txt similarity index 60% rename from test/CMakeLists.txt rename to tests/CMakeLists.txt index e8a0ca8..099c49a 100644 --- a/test/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,32 +1,3 @@ -add_executable(xvc_updater_tests) -target_sources(xvc_updater_tests - PRIVATE - updater_test_base.h - updater_test.cc -) -target_link_libraries(xvc_updater_tests - PRIVATE - updater - fmt::fmt - gtest::gtest -) -target_include_directories(xvc_updater_tests - INTERFACE - "$" - "$" -) - -add_test( - NAME xvc_updater_tests - COMMAND xvc_updater_tests -) -target_compile_features(xvc_updater_tests PRIVATE cxx_std_23) -target_compile_options(xvc_updater_tests - PRIVATE - $<$:/W4> - $<$>:-Wall> -) - add_executable(test_port_pool test_port_pool.cc) target_link_libraries(test_port_pool PRIVATE diff --git a/test/test_camera.cc b/tests/test_camera.cc similarity index 100% rename from test/test_camera.cc rename to tests/test_camera.cc diff --git a/test/test_port_pool.cc b/tests/test_port_pool.cc similarity index 100% rename from test/test_port_pool.cc rename to tests/test_port_pool.cc diff --git a/test/test_ws_client.cc b/tests/test_ws_client.cc similarity index 100% rename from test/test_ws_client.cc rename to tests/test_ws_client.cc diff --git a/tool/CMakeLists.txt b/tool/CMakeLists.txt deleted file mode 100644 index 305ba2a..0000000 --- a/tool/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -add_executable(tvcli tvcli.cc) -target_link_libraries(tvcli - PRIVATE - xvc - CLI11::CLI11 - PkgConfig::gstreamer-app - PkgConfig::gstreamer-video -) -target_compile_features(tvcli PRIVATE cxx_std_23) -target_compile_options(tvcli - PRIVATE - $<$:/W4> - $<$>:-Wall> -) - -add_executable(xvc_update_tool xvc_update_tool.cc) -target_link_libraries(xvc_update_tool - PRIVATE - updater - CLI11::CLI11 - spdlog::spdlog -) -target_compile_features(xvc_update_tool PRIVATE cxx_std_23) -target_compile_options(xvc_update_tool - PRIVATE - $<$:/W4> - $<$>:-Wall> -) \ No newline at end of file diff --git a/tool/xvc_update_tool.cc b/tool/xvc_update_tool.cc deleted file mode 100644 index bfe46b3..0000000 --- a/tool/xvc_update_tool.cc +++ /dev/null @@ -1,113 +0,0 @@ -#include - -#include -#include - -#include "common.h" -#include "updater.h" - -int main(int argc, char *argv[]) -{ - // TODO: Make this updater CLI part of tvcli - std::string server_address = "192.168.177.100"; - auto server_port = 8000; - auto update_server_port = 8001; - std::string version_table_url = "https://xvc001.sgp1.digitaloceanspaces.com/versions.json"; - std::string update_dir = "updates"; - auto skip_version_check = false; - std::string target_version; - std::string calculate_hash_file; - bool get_server_version = false; - - CLI::App app{"Thor Vision Server Updater"}; - app.add_option("-s,--server", server_address, "Server address (IP or hostname)") - ->default_val(server_address); - app.add_option("-p,--port", server_port, "Server port to be updated")->default_val(server_port); - app.add_option("-u,--update-port", update_server_port, "Update server port") - ->default_val(update_server_port); - app.add_option("-t,--version-table", version_table_url, "Version table URL") - ->default_val(version_table_url); - app.add_option("-d,--update-dir", update_dir, "Directory for downloaded updates") - ->default_val(update_dir); - app.add_flag("-f,--force", skip_version_check, "Skip version check"); - app.add_option("-v,--version", target_version, "Target version to update to (optional)"); - app.add_option("-c,--calculate-hash", calculate_hash_file, "Calculate SHA256 hash for a file"); - app.add_flag( - "-g,--get-server-version", get_server_version, "Get and display the server version" - ); - - CLI11_PARSE(app, argc, argv); - - try { - // Handle calculate-hash option - if (!calculate_hash_file.empty()) { - auto hash = xvc::calculate_sha256(calculate_hash_file); - if (hash) { - return EXIT_SUCCESS; - } else { - spdlog::error("Failed to calculate hash for file: {}", calculate_hash_file); - return EXIT_FAILURE; - } - } - - // Handle get-server-version option - if (get_server_version) { - auto version = xvc::get_server_version(server_address, server_port); - if (version) { - spdlog::info("Server version: {}", version->to_string()); - return EXIT_SUCCESS; - } else { - spdlog::error("Failed to get server version"); - return EXIT_FAILURE; - } - } - - // Set client version (using a dummy version that should work with most updates) - xvc::Version client_version{999, 999, 999}; - - // Parse target version if specified - std::optional force_version; - if (!target_version.empty()) { - auto parsed_version = xvc::Version::from_string(target_version); - if (!parsed_version) { - spdlog::error("Invalid target version format: {}", target_version); - return EXIT_FAILURE; - } - force_version = *parsed_version; - } - - // Perform update - auto result = xvc::update_server( - server_address, - server_port, - update_server_port, - version_table_url, - update_dir, - client_version, - skip_version_check, - force_version - ); - - if (!result.success) { - spdlog::error("Update failed: {}", result.error_message); - return EXIT_FAILURE; - } - - if (!result.update_needed) { - spdlog::info( - "Server is already up to date (version {})", result.current_version.to_string() - ); - return EXIT_SUCCESS; - } - - spdlog::info( - "Successfully updated server from {} to {}", - result.current_version.to_string(), - result.available_version.to_string() - ); - return EXIT_SUCCESS; - } catch (const std::exception &e) { - spdlog::error("Error: {}", e.what()); - return EXIT_FAILURE; - } -} \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..9e84aae --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(tvcli tvcli.cc) +target_link_libraries(tvcli + PRIVATE + xvc + xdaqmetadata::xdaqmetadata + CLI11::CLI11 + PkgConfig::gstreamer-app + PkgConfig::gstreamer-video +) +target_compile_features(tvcli PRIVATE cxx_std_23) +target_compile_options(tvcli + PRIVATE + $<$:/W4> + $<$>:-Wall> +) \ No newline at end of file diff --git a/tool/tvcli.cc b/tools/tvcli.cc similarity index 96% rename from tool/tvcli.cc rename to tools/tvcli.cc index 8696dcc..78e85f1 100644 --- a/tool/tvcli.cc +++ b/tools/tvcli.cc @@ -12,7 +12,7 @@ #include "camera.h" #include "server.h" -#include "xdaqmetadata/metadata_handler.h" +#include "xdaqmetadata/safe_queue.h" #include "xvc.h" namespace fs = std::filesystem; @@ -25,13 +25,13 @@ GstElement *pipeline = nullptr; bool record = false; std::unique_ptr stream_cam = nullptr; -std::unique_ptr handler = nullptr; +std::unique_ptr queue = nullptr; std::chrono::steady_clock::time_point stream_duration; enum class Codec : int { MJPEG }; enum class TimeUnit : int { Seconds, Minutes, Hours, Days }; -GstFlowReturn draw_image(GstAppSink *sink, [[maybe_unused]] void *user_data) +GstFlowReturn draw_image(GstAppSink *sink, void *) { std::unique_ptr sample( gst_app_sink_pull_sample(sink), gst_sample_unref @@ -55,7 +55,7 @@ GstFlowReturn draw_image(GstAppSink *sink, [[maybe_unused]] void *user_data) static_cast(g_value_get_int(gst_structure_get_value(structure, "height"))); const auto buffer_pts = GST_BUFFER_PTS(buffer); - auto xdaqmetadata = handler->_safe_queue.dequeue(buffer_pts); + auto xdaqmetadata = queue->dequeue(buffer_pts); if (!xdaqmetadata) { std::println("Failed to dequeue XDAQ metadata from buffer with PTS {}", buffer_pts); return GST_FLOW_OK; @@ -112,7 +112,6 @@ int func(int argc, char *argv[]) int id; std::string gst_cap; Codec codec{Codec::MJPEG}; - // TODO std::unordered_map codec_map{{"mjpeg", Codec::MJPEG}}; std::string location = "records"; @@ -174,7 +173,7 @@ int func(int argc, char *argv[]) gst_init(&argc, &argv); if (*stream) { - handler = std::make_unique(); + queue = std::make_unique(); pipeline = gst_pipeline_new(nullptr); loop = g_main_loop_new(nullptr, false); stream_duration = std::chrono::steady_clock::now(); @@ -234,7 +233,7 @@ int func(int argc, char *argv[]) gst_element_get_static_pad(parser, "src"), gst_object_unref ); gst_pad_add_probe( - src_pad.get(), GST_PAD_PROBE_TYPE_BUFFER, parse_jpeg_metadata, handler.get(), nullptr + src_pad.get(), GST_PAD_PROBE_TYPE_BUFFER, parse_jpeg_metadata, queue.get(), nullptr ); GstAppSinkCallbacks callbacks = {nullptr, nullptr, draw_image, nullptr, nullptr, {nullptr}}; @@ -252,7 +251,7 @@ int func(int argc, char *argv[]) gst_element_set_state(pipeline, GST_STATE_NULL); } stream_cam.reset(); - handler.reset(); + queue.reset(); } if (*list) { diff --git a/xdaqvc/CMakeLists.txt b/xdaqvc/CMakeLists.txt index 7f5c1b8..617b363 100644 --- a/xdaqvc/CMakeLists.txt +++ b/xdaqvc/CMakeLists.txt @@ -1,40 +1,33 @@ -add_library(xvc STATIC) - -set(sources - xvc.cc - camera.cc - port_pool.cc - ws_client.cc - server.cc - validator.cc -) -set(headers - xvc.h - camera.h - port_pool.h - ws_client.h - server.h - validator.h - common.h -) - target_sources(xvc PRIVATE - "${sources}" + ${CMAKE_CURRENT_SOURCE_DIR}/xvc.cc + ${CMAKE_CURRENT_SOURCE_DIR}/camera.cc + ${CMAKE_CURRENT_SOURCE_DIR}/port_pool.cc + # ${CMAKE_CURRENT_SOURCE_DIR}/stream.cc + ${CMAKE_CURRENT_SOURCE_DIR}/ws_client.cc + ${CMAKE_CURRENT_SOURCE_DIR}/server.cc + ${CMAKE_CURRENT_SOURCE_DIR}/validator.cc + ${CMAKE_CURRENT_SOURCE_DIR}/port_pool.h + ${CMAKE_CURRENT_SOURCE_DIR}/validator.h PUBLIC FILE_SET "public_headers" TYPE "HEADERS" - FILES "${headers}" -) -target_include_directories(xvc - INTERFACE - "$" - "$" -) -set_target_properties(xvc PROPERTIES - VERSION "${libxvc_VERSION}" - SOVERSION "${PROJECT_VERSION_MAJOR}" - POSITION_INDEPENDENT_CODE ON + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/xvc.h + ${CMAKE_CURRENT_SOURCE_DIR}/camera.h + ${CMAKE_CURRENT_SOURCE_DIR}/server.h + # ${CMAKE_CURRENT_SOURCE_DIR}/stream.h + ${CMAKE_CURRENT_SOURCE_DIR}/common.h + ${CMAKE_CURRENT_SOURCE_DIR}/ws_client.h +) +set_target_properties(xvc + PROPERTIES + # TODO + INSTALL_RPATH "@executable_path/../Frameworks;@loader_path/../Frameworks" + # VERSION "${libxvc_VERSION}" + # SOVERSION "${PROJECT_VERSION_MAJOR}" + POSITION_INDEPENDENT_CODE ON + CXX_VISIBILITY_PRESET hidden ) target_compile_features(xvc PUBLIC cxx_std_23) @@ -44,72 +37,17 @@ target_compile_options(xvc $<$>:-Wall> ) -if(CMAKE_BUILD_TYPE MATCHES "Debug") - if(MSVC) - message(STATUS "MSVC Debug not set ASAN options") - else() - target_compile_options(libxvc PUBLIC -fsanitize=address,undefined) - target_link_options(libxvc PUBLIC -fsanitize=address,undefined) - endif() -endif() - target_link_libraries(xvc PUBLIC Boost::boost - # TODO: - xdaqmetadata::xdaqmetadata + PkgConfig::gstreamer + nlohmann_json::nlohmann_json PRIVATE cpr::cpr spdlog::spdlog - nlohmann_json::nlohmann_json nlohmann_json_schema_validator - PkgConfig::gstreamer -) - -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "default install path" FORCE) -endif() - -install( - TARGETS xvc - EXPORT libxvc-targets - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - FILE_SET "public_headers" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/xdaqvc" -) -install( - EXPORT libxvc-targets - FILE libxvc-targets.cmake - NAMESPACE libxvc:: - DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/libxvc" ) -export( - EXPORT libxvc-targets - FILE libxvc-config.cmake - NAMESPACE libxvc:: -) - -add_library(updater STATIC) -target_sources(updater - PRIVATE - updater.cc - PUBLIC - FILE_SET "public_headers" - TYPE "HEADERS" - FILES updater.h -) -target_compile_features(updater PUBLIC cxx_std_23) -target_compile_options(updater - PRIVATE - $<$:/W4> - $<$>:-Wall> -) -target_link_libraries(updater - PRIVATE - cpr::cpr - spdlog::spdlog - nlohmann_json::nlohmann_json - OpenSSL::SSL -) \ No newline at end of file +# if(BUILD_SHARED_LIBS) +# target_compile_definitions(xvc PRIVATE "xvc_EXPORTS") +# endif() \ No newline at end of file diff --git a/xdaqvc/camera.cc b/xdaqvc/camera.cc index b63980b..0f35e91 100644 --- a/xdaqvc/camera.cc +++ b/xdaqvc/camera.cc @@ -17,7 +17,6 @@ constexpr std::string_view URL = "http://192.168.177.100:8000"; constexpr std::string_view CAMERAS = "/cameras"; constexpr std::string_view MJPEG = "/jpeg"; constexpr std::string_view H265 = "/h265"; -constexpr std::string_view H264 = "/h264"; constexpr std::string_view STOP = "/stop"; constexpr auto OK = 200; diff --git a/xdaqvc/camera.h b/xdaqvc/camera.h index 781186b..b7979d6 100644 --- a/xdaqvc/camera.h +++ b/xdaqvc/camera.h @@ -55,7 +55,9 @@ class Camera void set_name(std::string_view name) { _name = name; } void add_cap(const Cap &cap) { _caps.emplace_back(cap); } - bool start(const Cap &cap, std::chrono::milliseconds duration = std::chrono::milliseconds(1000)); + bool start( + const Cap &cap, std::chrono::milliseconds duration = std::chrono::milliseconds(1000) + ); bool stop(std::chrono::milliseconds duration = std::chrono::milliseconds(1000)); private: diff --git a/xdaqvc/server.h b/xdaqvc/server.h index dfcd54c..a372adc 100644 --- a/xdaqvc/server.h +++ b/xdaqvc/server.h @@ -15,15 +15,15 @@ class Server public: explicit Server(std::string_view host = "192.168.177.100", int port = 8000) noexcept; - bool root(std::chrono::milliseconds timeout = std::chrono::milliseconds(500)) const; + bool root(std::chrono::milliseconds timeout = std::chrono::milliseconds(1000)) const; std::optional logs( std::string_view filename = "", - std::chrono::milliseconds timeout = std::chrono::milliseconds(500) + std::chrono::milliseconds timeout = std::chrono::milliseconds(1000) ) const; std::optional api_version( - std::chrono::milliseconds timeout = std::chrono::milliseconds(500) + std::chrono::milliseconds timeout = std::chrono::milliseconds(1000) ) const; private: diff --git a/xdaqvc/updater.cc b/xdaqvc/updater.cc deleted file mode 100644 index 9941c7c..0000000 --- a/xdaqvc/updater.cc +++ /dev/null @@ -1,622 +0,0 @@ -#include "updater.h" - -#include -#include -#include - -#include -#include -#include - - -using namespace std::chrono_literals; -namespace fs = std::filesystem; - -namespace -{ -auto constexpr OK = 200; -} // namespace - - -namespace xvc -{ -std::optional calculate_sha256(const fs::path &filepath) -{ - std::ifstream file(filepath, std::ios::binary); - if (!file) { - spdlog::error("Failed to open file for hashing: {}", filepath.string()); - return std::nullopt; - } - - SHA256_CTX sha256; - SHA256_Init(&sha256); - - char buffer[4096]; - while (file.read(buffer, sizeof(buffer))) { - SHA256_Update(&sha256, buffer, file.gcount()); - } - if (file.gcount() > 0) { - SHA256_Update(&sha256, buffer, file.gcount()); - } - - unsigned char hash[SHA256_DIGEST_LENGTH]; - SHA256_Final(hash, &sha256); - - std::stringstream ss; - ss << std::hex << std::setfill('0'); - for (size_t i = 0; i < SHA256_DIGEST_LENGTH; i++) { - ss << std::setw(2) << static_cast(hash[i]); - } - - auto calculated = ss.str(); - spdlog::info("Calculated hash: {}", calculated); - return calculated; -} - -DownloadResult download_and_verify( - const std::string &url, const std::string &expected_hash, const fs::path &output_path -) -{ - DownloadResult result{false, ""}; - constexpr auto MAX_RETRIES = 3; - constexpr std::chrono::seconds TIMEOUT{30}; - - try { - // First, make a HEAD request to get the expected file size - auto head_response = cpr::Head(cpr::Url{url}, cpr::VerifySsl{false}, cpr::Timeout{2s}); - if (head_response.status_code != OK) { - result.error_message = - fmt::format("Failed to get file information: {}", head_response.status_code); - return result; - } - - // Get expected file size from header - size_t expected_size = 0; - if (head_response.header.count("Content-Length") > 0) { - expected_size = std::stoull(head_response.header["Content-Length"]); - } - - // Retry loop - for (int attempt = 1; attempt <= MAX_RETRIES; ++attempt) { - std::ofstream file(output_path, std::ios::binary); - if (!file) { - result.error_message = "Failed to open output file for writing"; - return result; - } - - // Track last reported progress - size_t last_progress = 0; - - // Progress callback - auto progress_callback = [&last_progress]( - size_t downloadTotal, - size_t downloadNow, - [[maybe_unused]] size_t uploadTotal, - [[maybe_unused]] size_t uploadNow, - [[maybe_unused]] intptr_t userdata - ) -> bool { - if (downloadTotal > 0) { - float progress_percentage = - static_cast(downloadNow) / downloadTotal * 100.0f; - if (downloadNow != last_progress) { - last_progress = downloadNow; - spdlog::info( - "Download progress: {:.1f}% ({}/{} bytes)", - progress_percentage, - downloadNow, - downloadTotal - ); - } - } - return true; // Continue transfer - }; - - // Perform the download - auto response = cpr::Download( - file, - cpr::Url{url}, - cpr::VerifySsl{false}, - cpr::Timeout{TIMEOUT}, - cpr::ProgressCallback(progress_callback) - ); - - file.close(); - - if (response.status_code != OK) { - spdlog::warn( - "Download attempt {} failed with status code: {}. Retrying...", - attempt, - response.status_code - ); - std::this_thread::sleep_for(std::chrono::seconds(attempt)); - continue; - } - - // Verify file size - if (expected_size > 0) { - auto actual_size = fs::file_size(output_path); - if (actual_size != expected_size) { - spdlog::warn( - "File size mismatch. Expected: {}, Got: {}. Retrying...", - expected_size, - actual_size - ); - fs::remove(output_path); - continue; - } - } - - // Calculate and verify hash - auto calculated_hash = calculate_sha256(output_path); - if (!calculated_hash) { - spdlog::warn("Failed to calculate file hash. Retrying..."); - fs::remove(output_path); - continue; - } - - if (*calculated_hash != expected_hash) { - spdlog::warn( - "Hash verification failed. Expected: {}, Got: {}. Retrying...", - expected_hash, - *calculated_hash - ); - fs::remove(output_path); - continue; - } - - // If we get here, all verifications passed - result.success = true; - return result; - } - - // If retries are exhausted - result.error_message = "Failed to download file after multiple attempts."; - return result; - - } catch (const std::exception &e) { - result.error_message = fmt::format("Download failed: {}", e.what()); - if (fs::exists(output_path)) { - fs::remove(output_path); - } - return result; - } -} - - -HandshakeResponse perform_handshake(const std::string &server_address, int port) -{ - HandshakeResponse response{false, "", "", {}}; - - try { - auto url = fmt::format("http://{}:{}/handshake", server_address, port); - - spdlog::info("Attempting handshake with server at {}", url); - - auto http_response = - cpr::Get(cpr::Url{url}, cpr::Timeout{2s}, cpr::Header{{"User-Agent", "XVC-Client"}}); - - if (http_response.status_code == OK) { - try { - auto json_response = nlohmann::json::parse(http_response.text); - spdlog::debug("Raw server response: {}", http_response.text); - - if (json_response["status"] == "ready") { - response.success = true; - response.token = json_response["token"].get(); - - // Get current UTC time - auto now = std::chrono::system_clock::now(); - auto now_ts = std::chrono::system_clock::to_time_t(now); - - // Get expiration time from server (as UTC timestamp) - auto expire_timestamp = json_response["expires"].get(); - response.expires = std::chrono::system_clock::from_time_t(expire_timestamp); - - auto expire_time_t = static_cast(expire_timestamp); - - // Format times in UTC - std::tm now_tm_utc{}, expire_tm_utc{}; -#ifdef _WIN32 - gmtime_s(&now_tm_utc, &now_ts); - gmtime_s(&expire_tm_utc, &expire_timestamp); -#else - gmtime_r(&now_ts, &now_tm_utc); - gmtime_r(&expire_time_t, &expire_tm_utc); -#endif - char now_str[32], expire_str[32]; - std::strftime(now_str, sizeof(now_str), "%Y-%m-%d %H:%M:%S UTC", &now_tm_utc); - std::strftime( - expire_str, sizeof(expire_str), "%Y-%m-%d %H:%M:%S UTC", &expire_tm_utc - ); - - spdlog::info("Handshake successful!"); - spdlog::info("Current UTC time: {}", now_str); - spdlog::info("Current UTC timestamp: {}", now_ts); - spdlog::info("Session token: {}", response.token); - spdlog::info("Token expires (UTC): {}", expire_str); - spdlog::info("Expire UTC timestamp: {}", expire_timestamp); - spdlog::info("Time until expiration: {} seconds", expire_timestamp - now_ts); - } - } catch (const nlohmann::json::exception &e) { - response.error_message = - fmt::format("Invalid handshake response format: {}", e.what()); - spdlog::error("JSON parse error: {}", e.what()); - } - } else { - response.error_message = - fmt::format("Handshake failed with status code: {}", http_response.status_code); - spdlog::error("HTTP error: {}", response.error_message); - } - - } catch (const std::exception &e) { - response.error_message = fmt::format("Handshake failed with exception: {}", e.what()); - spdlog::error("Exception: {}", e.what()); - } - - return response; -} - -bool prepare_file_transfer( - const std::string &server_address, int port, const std::string &token, - const std::string &filename, const std::string &file_hash, size_t file_size, - std::string &out_transfer_id -) -{ - try { - auto url = fmt::format("http://{}:{}/prepare-transfer", server_address, port); - - nlohmann::json request_body = { - {"filename", filename}, {"file_hash", file_hash}, {"file_size", file_size} - }; - - auto response = cpr::Post( - cpr::Url{url}, - cpr::Header{ - {"Authorization", fmt::format("Bearer {}", token)}, - {"Content-Type", "application/json"} - }, - cpr::Body{request_body.dump()}, - cpr::Timeout{5s} - ); - - if (response.status_code == OK) { - auto json_response = nlohmann::json::parse(response.text); - if (json_response["status"] == "ready") { - out_transfer_id = json_response["transfer_id"]; - return true; - } - } - - spdlog::error("Failed to prepare transfer: {} ({})", response.text, response.status_code); - return false; - - } catch (const std::exception &e) { - spdlog::error("Error preparing transfer: {}", e.what()); - return false; - } -} - -bool transfer_file( - const std::string &server_address, int port, const std::string &token, - const fs::path &file_path, const std::string &transfer_id, - std::function progress_callback -) -{ - auto timeout = std::chrono::seconds{30}; - try { - if (!fs::exists(file_path)) { - spdlog::error("File does not exist: {}", file_path.string()); - return false; - } - if (!fs::is_regular_file(file_path)) { - spdlog::error("Invalid file type: {}", file_path.string()); - return false; - } - - auto file_size = fs::file_size(file_path); - if (file_size == 0) { - spdlog::error("File is empty: {}", file_path.string()); - return false; - } - - auto url = fmt::format("http://{}:{}/transfer/{}", server_address, port, transfer_id); - - cpr::Multipart multipart{}; - multipart.parts.emplace_back("file", cpr::File{file_path.string()}); - - cpr::Header headers = {{"Authorization", fmt::format("Bearer {}", token)}}; - - // Deduplication logic in progress callback - auto progress_callback_wrapper = [&progress_callback, file_size]( - size_t, size_t, size_t, size_t ul_now, intptr_t - ) -> bool { - static size_t last_progress = 0; - auto actual_progress = std::clamp(ul_now, size_t{0}, file_size); - - if (actual_progress != last_progress) { // Only log/report if progress changes - last_progress = actual_progress; - if (progress_callback) { - progress_callback( - {actual_progress, - file_size, - file_size > 0 ? static_cast(actual_progress) / file_size * 100.0f - : 0.0f} - ); - } - } - return true; // Continue transfer - }; - - auto response = cpr::Post( - cpr::Url{url}, - headers, - multipart, - progress_callback ? cpr::ProgressCallback(progress_callback_wrapper) - : cpr::ProgressCallback{}, - cpr::Timeout{timeout} - ); - - if (response.status_code == OK) { - auto json_response = nlohmann::json::parse(response.text); - return json_response["status"] == "success"; - } else { - spdlog::error( - "File transfer failed with status code: {} ({})", - response.status_code, - response.text - ); - return false; - } - - } catch (const std::exception &e) { - spdlog::error( - "File transfer failed for file '{}' (transfer_id: {}): {}", - file_path.string(), - transfer_id, - e.what() - ); - return false; - } -} - - -std::optional get_server_version(const std::string &server_address, int port) -{ - try { - auto response = cpr::Get( - cpr::Url{fmt::format("http://{}:{}/server_version", server_address, port)}, - cpr::Timeout{5s} - ); - - if (response.status_code == OK) { - // Parse JSON response - auto json_response = nlohmann::json::parse(response.text); - - // Extract version string from JSON - if (json_response.contains("version")) { - return Version::from_string(json_response["version"].get()); - } - - spdlog::error("Server response missing version field: {}", response.text); - return std::nullopt; - } - - spdlog::error("Failed to get server version: {} ({})", response.text, response.status_code); - return std::nullopt; - - } catch (const std::exception &e) { - spdlog::error("Error getting server version: {}", e.what()); - return std::nullopt; - } -} - -std::optional get_version_table(const std::string &table_url) -{ - try { - auto response = cpr::Get( - cpr::Url{table_url}, cpr::Timeout{5s}, cpr::VerifySsl{false} - // Add this if needed for self-signed certs - ); - - if (response.status_code == OK) { - auto json = nlohmann::json::parse(response.text); - VersionTable table; - - // Parse latest version - auto latest_ver = Version::from_string(json["latest_version"].get()); - if (!latest_ver) { - spdlog::error("Invalid latest version format"); - return std::nullopt; - } - table.latest_version = *latest_ver; - - // Parse version entries - for (const auto &version_data : json["versions"]) { - UpdateInfo info; - - // Parse version - auto ver = Version::from_string(version_data["version"].get()); - if (!ver) { - spdlog::error("Invalid version format in version entry"); - continue; - } - info.version = *ver; - - // Parse other fields - info.release_date = version_data["release_date"].get(); - info.update_url = version_data["update_url"].get(); - info.hash = version_data["hash"].get(); - - auto min_ver = - Version::from_string(version_data["min_client_version"].get()); - if (!min_ver) { - spdlog::error("Invalid min_client_version format"); - continue; - } - info.min_client_version = *min_ver; - - info.description = version_data["description"].get(); - - table.versions.push_back(info); - } - - return table; - } - - spdlog::error("Failed to get version table: {} ({})", response.text, response.status_code); - return std::nullopt; - - } catch (const std::exception &e) { - spdlog::error("Error getting version table: {}", e.what()); - return std::nullopt; - } -} - -UpdateResult update_server( - const std::string &server_address, - int server_port, // Port of the server to be updated - int update_server_port, // Port of the update server - const std::string &table_url, const fs::path &update_dir, - [[maybe_unused]] const Version &client_version, bool skip_version_check, - const std::optional &force_version -) -{ - UpdateResult result; - result.success = false; - - try { - // Step 1: Version check (unless skipped) - - auto current_version = get_server_version(server_address, server_port); - if (!current_version) { - result.error_message = "Failed to get current server version"; - return result; - } - result.current_version = *current_version; - - - // Step 2: Get version table - auto version_table = get_version_table(table_url); - if (!version_table) { - result.error_message = "Failed to get version table"; - return result; - } - - // If force_version is specified, override the target version - if (force_version) { - auto it = std::find_if( - version_table->versions.begin(), - version_table->versions.end(), - [&](const UpdateInfo &info) { return info.version == *force_version; } - ); - - if (it == version_table->versions.end()) { - result.error_message = fmt::format( - "Forced version {} not found in version table", force_version->to_string() - ); - return result; - } - version_table->latest_version = *force_version; - } - - result.available_version = version_table->latest_version; - - // Check if update is needed (unless forced) - if (!skip_version_check && result.current_version >= version_table->latest_version) { - result.update_needed = false; - result.success = true; - return result; - } - - result.update_needed = true; - - // Step 3: Download and verify update file - auto target_version = std::find_if( - version_table->versions.begin(), - version_table->versions.end(), - [&](const UpdateInfo &info) { return info.version == version_table->latest_version; } - ); - - if (target_version == version_table->versions.end()) { - result.error_message = "Target version not found in version table"; - return result; - } - - // Create update directory if it doesn't exist - fs::create_directories(update_dir); - - auto update_file = - update_dir / fmt::format("xvc-server-{}.tar.xz", target_version->version.to_string()); - - spdlog::info("Downloading update file from {}", target_version->update_url); - auto download_result = - download_and_verify(target_version->update_url, target_version->hash, update_file); - - if (!download_result.success) { - result.error_message = - fmt::format("Failed to download update: {}", download_result.error_message); - return result; - } - - // Step 4: Perform handshake with update server - spdlog::info("Performing handshake with update server"); - auto handshake_response = perform_handshake(server_address, update_server_port); - if (!handshake_response.success) { - result.error_message = - fmt::format("Handshake failed: {}", handshake_response.error_message); - return result; - } - - // Step 5: Prepare file transfer - std::string transfer_id; - auto file_size = fs::file_size(update_file); - - spdlog::info("Preparing file transfer"); - bool prepared = prepare_file_transfer( - server_address, - update_server_port, - handshake_response.token, - update_file.filename().string(), - target_version->hash, - file_size, - transfer_id - ); - - if (!prepared) { - result.error_message = "Failed to prepare file transfer"; - return result; - } - - // Step 6: Perform file transfer - spdlog::info("Transferring update file"); - bool transfer_success = transfer_file( - server_address, - update_server_port, - handshake_response.token, - update_file, - transfer_id, - [](const FileTransferProgress &progress) { - spdlog::info( - "Transfer progress: {:.1f}% ({}/{} bytes)", - progress.progress_percentage, - progress.bytes_transferred, - progress.total_bytes - ); - } - ); - - if (!transfer_success) { - result.error_message = "File transfer failed"; - return result; - } - - result.success = true; - return result; - - } catch (const std::exception &e) { - result.error_message = fmt::format("Update failed: {}", e.what()); - return result; - } -} - -} // namespace xvc \ No newline at end of file diff --git a/xdaqvc/updater.h b/xdaqvc/updater.h deleted file mode 100644 index f73945e..0000000 --- a/xdaqvc/updater.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "common.h" - - -namespace xvc -{ - -struct DownloadResult { - bool success; - std::string error_message; -}; - -struct HandshakeResponse { - bool success; - std::string token; - std::string error_message; - std::chrono::system_clock::time_point expires; -}; - -struct FileTransferProgress { - size_t bytes_transferred; - size_t total_bytes; - float progress_percentage; -}; - -struct UpdateInfo { - Version version; - std::string release_date; - std::string update_url; - std::string hash; - Version min_client_version; - std::string description; -}; - -struct VersionTable { - Version latest_version; - std::vector versions; -}; - -struct UpdateResult { - bool success; - std::string error_message; - Version current_version; - Version available_version; - bool update_needed; -}; - -DownloadResult download_and_verify( - const std::string &url, const std::string &expected_hash, - const std::filesystem::path &output_path -); - -std::optional calculate_sha256(const std::filesystem::path &filepath); - -HandshakeResponse perform_handshake(const std::string &server_address, int port); - -bool prepare_file_transfer( - const std::string &server_address, int port, const std::string &token, - const std::string &filename, const std::string &file_hash, size_t file_size, - std::string &out_transfer_id -); - -bool transfer_file( - const std::string &server_address, int port, const std::string &token, - const std::filesystem::path &file_path, const std::string &transfer_id, - std::function progress_callback = nullptr -); - -// Get server version -std::optional get_server_version(const std::string &server_address, int port); - -// Get version table from CDN -std::optional get_version_table(const std::string &table_url); - -// Main update function -UpdateResult update_server( - const std::string &server_address, - int server_port, // Port of the server to be updated - int update_server_port, // Port of the update server - const std::string &table_url, const std::filesystem::path &update_dir, - const Version &client_version, bool skip_version_check = false, - const std::optional &force_version = std::nullopt -); - -} // namespace xvc \ No newline at end of file diff --git a/xdaqvc/xvc.cc b/xdaqvc/xvc.cc index 8b3c9bd..de3234c 100644 --- a/xdaqvc/xvc.cc +++ b/xdaqvc/xvc.cc @@ -7,24 +7,12 @@ #include #include -#include "xdaqmetadata/key_value_store.h" -#include "xdaqmetadata/xdaqmetadata.h" - using namespace std::chrono_literals; namespace fs = std::filesystem; namespace { -GstElement *create_element(const gchar *factoryname, const gchar *name) -{ - auto element = gst_element_factory_make(factoryname, name); - if (!element) { - spdlog::error("Element {} could not be created.", factoryname); - } - return element; -} - struct FileTracker { std::string base_filepath; std::vector file_paths; @@ -65,109 +53,27 @@ gchararray generate_filename(GstElement *, guint, gpointer udata) namespace xvc { -void setup_h265_srt_stream(GstPipeline *pipeline, const std::string &uri) -{ - spdlog::info("Setup GStreamer H.265 SRT stream pipeline"); - - auto src = create_element("srtsrc", "src"); - auto parser = create_element("h265parse", "parser"); - auto cf_parser = create_element("capsfilter", "cf_parser"); - auto tee = create_element("tee", "t"); - auto queue_display = create_element("queue", "queue_display"); -#ifdef _WIN32 - auto dec = create_element("d3d11h265dec", "dec"); -#elif __APPLE__ - auto dec = create_element("vtdec", "dec"); -#else - auto dec = create_element("avdec_h265", "dec"); -#endif - auto cf_dec = create_element("capsfilter", "cf_dec"); - auto conv = create_element("videoconvert", "conv"); - auto cf_conv = create_element("capsfilter", "cf_conv"); - auto appsink = create_element("appsink", "appsink"); - - // clang-format off - std::unique_ptr cf_src_caps( - gst_caps_new_simple( - "application/x-rtp", - "encoding-name", G_TYPE_STRING, "H265", - nullptr), - gst_caps_unref - ); - std::unique_ptr cf_parser_caps( - gst_caps_new_simple( - "video/x-h265", - "stream-format", G_TYPE_STRING, "byte-stream", - "alignment", G_TYPE_STRING, "au", - nullptr), - gst_caps_unref - ); - std::unique_ptr cf_dec_caps( - gst_caps_new_simple( - "video/x-raw", - "format", G_TYPE_STRING, "NV12", - nullptr), - gst_caps_unref - ); - std::unique_ptr cf_conv_caps( - gst_caps_new_simple( - "video/x-raw", - "format", G_TYPE_STRING, "RGB", - nullptr), - gst_caps_unref - ); - // clang-format on - - g_object_set(G_OBJECT(src), "uri", std::format("srt://{}", uri).c_str(), nullptr); - g_object_set(G_OBJECT(cf_parser), "caps", cf_parser_caps.get(), nullptr); - g_object_set(G_OBJECT(cf_dec), "caps", cf_dec_caps.get(), nullptr); - g_object_set(G_OBJECT(cf_conv), "caps", cf_conv_caps.get(), nullptr); - - gst_bin_add_many( - GST_BIN(pipeline), - src, - parser, - cf_parser, - tee, - queue_display, - dec, - cf_dec, - conv, - cf_conv, - appsink, - nullptr - ); - - if (!gst_element_link_many(src, parser, cf_parser, tee, nullptr) || - !gst_element_link_many(tee, queue_display, dec, cf_dec, conv, cf_conv, appsink, nullptr)) { - spdlog::error("Elements could not be linked."); - gst_object_unref(pipeline); - } -} void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri) { - if (!pipeline) { - spdlog::error("Pipeline is null"); - return; - } + if (!pipeline) return; spdlog::info("Setup GStreamer M-JPEG SRT stream pipeline with uri: {}", uri); - auto src = create_element("srtclientsrc", "src"); - auto parser = create_element("jpegparse", "parser"); - auto tee = create_element("tee", "t"); - auto queue_display = create_element("queue", "queue_display"); + auto src = gst_element_factory_make("srtclientsrc", "src"); + auto parser = gst_element_factory_make("jpegparse", "parser"); + auto tee = gst_element_factory_make("tee", "t"); + auto queue_display = gst_element_factory_make("queue", "queue_display"); #ifdef _WIN32 - auto dec = create_element("jpegdec", "dec"); + auto dec = gst_element_factory_make("jpegdec", "dec"); #elif __APPLE__ - auto dec = create_element("vtdec", "dec"); + auto dec = gst_element_factory_make("vtdec", "dec"); #else - auto dec = create_element("jpegdec", "dec"); + auto dec = gst_element_factory_make("jpegdec", "dec"); #endif - auto conv = create_element("videoconvert", "conv"); - auto cf_conv = create_element("capsfilter", "cf_conv"); - auto fpsdisplaysink = create_element("fpsdisplaysink", "fpsdisplaysink"); - auto appsink = create_element("appsink", "appsink"); + auto conv = gst_element_factory_make("videoconvert", "conv"); + auto cf_conv = gst_element_factory_make("capsfilter", "cf_conv"); + auto fpsdisplaysink = gst_element_factory_make("fpsdisplaysink", "fpsdisplaysink"); + auto appsink = gst_element_factory_make("appsink", "appsink"); // clang-format off std::unique_ptr cf_conv_caps( @@ -182,9 +88,15 @@ void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri) g_object_set(G_OBJECT(src), "uri", std::format("srt://{}", uri).c_str(), nullptr); g_object_set(G_OBJECT(cf_conv), "caps", cf_conv_caps.get(), nullptr); g_object_set(G_OBJECT(appsink), "sync", false, nullptr); - g_object_set(G_OBJECT(fpsdisplaysink), "video-sink", appsink, nullptr); - g_object_set(G_OBJECT(fpsdisplaysink), "text-overlay", false, nullptr); - g_object_set(G_OBJECT(fpsdisplaysink), "sync", false, nullptr); + // clang-format off + g_object_set( + G_OBJECT(fpsdisplaysink), + "video-sink", appsink, + "text-overlay", false, + "sync", false, + nullptr + ); + // clang-format on gst_bin_add_many( GST_BIN(pipeline), @@ -206,153 +118,14 @@ void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri) } } -void start_h265_recording( - GstPipeline *pipeline, fs::path &filepath, bool continuous, int max_size_time, int max_files -) -{ - spdlog::info("Start GStreamer H.265 recording"); - - auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - auto src_pad = gst_element_request_pad_simple(tee, "src_1"); - - auto queue_record = create_element("queue", "queue_record"); - auto parser = create_element("h265parse", "record_parser"); - auto cf_parser = create_element("capsfilter", "cf_record_parser"); - auto filesink = create_element("splitmuxsink", "filesink"); - - filepath += continuous ? ".mkv" : "-%02d.mkv"; - auto _max_size_time = continuous ? 0 : max_size_time * GST_SECOND * 60; - - // clang-format off - std::unique_ptr cf_parser_caps( - gst_caps_new_simple( - "video/x-h265", - "stream-format", G_TYPE_STRING, "hvc1", - "alignment", G_TYPE_STRING, "au", - nullptr), - gst_caps_unref - ); - // clang-format on - - g_object_set(G_OBJECT(cf_parser), "caps", cf_parser_caps.get(), nullptr); - g_object_set(G_OBJECT(filesink), "location", filepath.generic_string().c_str(), nullptr); - g_object_set( - G_OBJECT(filesink), "max-size-time", _max_size_time, nullptr - ); // max-size-time=0 -> continuous - g_object_set(G_OBJECT(filesink), "max-files", max_files, nullptr); - g_object_set( - G_OBJECT(filesink), "max-size-bytes", 0, nullptr - ); // Set max-size-bytes to 0 in order to make send-keyframe-requests work. - g_object_set(G_OBJECT(filesink), "send-keyframe-requests", true, nullptr); - g_object_set(G_OBJECT(filesink), "async-finalize", true, nullptr); - g_object_set( - G_OBJECT(filesink), "muxer-factory", "matroskamux", nullptr - ); // Valid only for async-finalize = TRUE - - - gst_bin_add_many(GST_BIN(pipeline), queue_record, parser, cf_parser, filesink, nullptr); - - if (!gst_element_link_many(queue_record, parser, cf_parser, filesink, nullptr)) { - spdlog::error("Elements could not be linked."); - gst_object_unref(pipeline); - return; - } - - gst_element_sync_state_with_parent(queue_record); - gst_element_sync_state_with_parent(parser); - gst_element_sync_state_with_parent(cf_parser); - gst_element_sync_state_with_parent(filesink); - - std::unique_ptr sink_pad( - gst_element_get_static_pad(queue_record, "sink"), gst_object_unref - ); - - auto ret = gst_pad_link(src_pad, sink_pad.get()); - if (GST_PAD_LINK_FAILED(ret)) { - spdlog::error("Failed to link 'tee' src pad to 'queue' sink pad"); - gst_object_unref(pipeline); - return; - } - GST_DEBUG_BIN_TO_DOT_FILE( - GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "video-capture-after-link" - ); -} - -void stop_h265_recording(GstPipeline *pipeline) -{ - spdlog::info("Stop GStreamer H.265 recording"); - - auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - auto src_pad = gst_element_get_static_pad(tee, "src_1"); - - // Increase the reference count so that 'pipeline' remains valid. - gst_object_ref(pipeline); - - gst_pad_add_probe( - src_pad, - GST_PAD_PROBE_TYPE_IDLE, - [](GstPad *src_pad, GstPadProbeInfo *, gpointer user_data) -> GstPadProbeReturn { - spdlog::info("Unlinking"); - - auto pipeline = GST_PIPELINE(user_data); - auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - std::unique_ptr queue_record( - gst_bin_get_by_name(GST_BIN(pipeline), "queue_record"), gst_object_unref - ); - std::unique_ptr parser( - gst_bin_get_by_name(GST_BIN(pipeline), "record_parser"), gst_object_unref - ); - std::unique_ptr cf_parser( - gst_bin_get_by_name(GST_BIN(pipeline), "cf_record_parser"), gst_object_unref - ); - std::unique_ptr filesink( - gst_bin_get_by_name(GST_BIN(pipeline), "filesink"), gst_object_unref - ); - std::unique_ptr sink_pad( - gst_element_get_static_pad(queue_record.get(), "sink"), gst_object_unref - ); - gst_pad_unlink(src_pad, sink_pad.get()); - gst_pad_send_event(sink_pad.get(), gst_event_new_eos()); - - // Launch a detached thread to remove the elements after a delay. - std::thread([pipeline, // captured pipeline (ref'ed) - queue_record = std::move(queue_record), - parser = std::move(parser), - cf_parser = std::move(cf_parser), - filesink = std::move(filesink)]() { - std::this_thread::sleep_for(std::chrono::milliseconds(3500)); - gst_bin_remove(GST_BIN(pipeline), queue_record.get()); - gst_bin_remove(GST_BIN(pipeline), parser.get()); - gst_bin_remove(GST_BIN(pipeline), cf_parser.get()); - gst_bin_remove(GST_BIN(pipeline), filesink.get()); - - gst_element_set_state(queue_record.get(), GST_STATE_NULL); - gst_element_set_state(parser.get(), GST_STATE_NULL); - gst_element_set_state(cf_parser.get(), GST_STATE_NULL); - gst_element_set_state(filesink.get(), GST_STATE_NULL); - - // Release the extra reference on the pipeline. - gst_object_unref(pipeline); - }).detach(); - - gst_element_release_request_pad(tee, src_pad); - gst_object_unref(src_pad); - - return GST_PAD_PROBE_REMOVE; - }, - pipeline, - nullptr - ); -} - bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) { if (!pipeline) { - spdlog::error("Pipeline is null"); + spdlog::debug("Pipeline is null"); return false; } if (config._path.empty()) { - spdlog::error("filepath is empty"); + spdlog::debug("Config path is empty"); return false; } spdlog::info("Starting M-JPEG recording ..."); @@ -361,8 +134,6 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) const auto split = config._split; const auto max_size_time = config._max_size_time; - spdlog::info("max_size_time = {}", max_size_time.count()); - auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); if (auto exist_tee_srcpad = gst_element_get_static_pad(tee, "src_1")) { spdlog::warn("tee 'src_1' pad already exists, releasing it..."); @@ -370,17 +141,34 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) gst_object_unref(exist_tee_srcpad); } auto tee_srcpad = gst_element_request_pad_simple(tee, "src_1"); + gst_object_unref(tee); - auto queue = create_element("queue", "queue_record"); - auto parser = create_element("jpegparse", "record_parser"); - auto muxer = create_element("matroskamux", "muxer"); - auto filesink = create_element("splitmuxsink", "filesink"); + auto queue = gst_element_factory_make("queue", "queue_record"); + auto parser = gst_element_factory_make("jpegparse", "record_parser"); + auto muxer = gst_element_factory_make("matroskamux", "muxer"); + auto filesink = gst_element_factory_make("splitmuxsink", "filesink"); + + if (!queue || !parser || !muxer || !filesink) { + spdlog::error("Failed to create elements for recording"); + if (queue) gst_object_unref(queue); + if (parser) gst_object_unref(parser); + if (muxer) gst_object_unref(muxer); + if (filesink) gst_object_unref(filesink); + return false; + } auto tracker = new FileTracker(location, {}, INT_MAX); - g_object_set_data_full(G_OBJECT(filesink), "file-tracker", tracker, [](gpointer data) { - delete static_cast(data); - }); - g_signal_connect(filesink, "format-location", G_CALLBACK(generate_filename), tracker); + g_signal_connect_data( + filesink, + "format-location", + G_CALLBACK(generate_filename), + tracker, + [](gpointer data, GClosure *) { + delete static_cast(data); + spdlog::debug("FileTracker deleted"); + }, + G_CONNECT_DEFAULT + ); // clang-format off g_object_set( @@ -401,7 +189,7 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) gst_bin_add_many(GST_BIN(pipeline), queue, parser, filesink, nullptr); if (!gst_element_link_many(queue, parser, filesink, nullptr)) { - spdlog::error("Elements could not be linked"); + spdlog::error("Failed to link MJPEG recording elements"); return false; } @@ -415,7 +203,6 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) return false; } gst_object_unref(queue_sinkpad); - gst_object_unref(tee); GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "after-link"); return true; @@ -424,20 +211,16 @@ bool start_jpeg_recording(GstPipeline *pipeline, const RecordConfig &config) bool stop_jpeg_recording(GstPipeline *pipeline) { if (!pipeline) { - spdlog::error("Pipeline is null"); + spdlog::debug("Pipeline is null"); return false; } spdlog::info("Stopping M-JPEG recording ..."); auto tee = gst_bin_get_by_name(GST_BIN(pipeline), "t"); - if (!tee) { - spdlog::error("Failed to get 'tee' element from pipeline"); - return false; - } + if (!tee) return false; auto tee_srcpad = gst_element_get_static_pad(tee, "src_1"); if (!tee_srcpad) { - spdlog::error("Failed to get 'tee' src_1 pad"); gst_object_unref(tee); return false; } @@ -445,14 +228,20 @@ bool stop_jpeg_recording(GstPipeline *pipeline) gst_pad_add_probe( tee_srcpad, GST_PAD_PROBE_TYPE_IDLE, - [](GstPad *tee_srcpad, - [[maybe_unused]] GstPadProbeInfo *info, - gpointer user_data) -> GstPadProbeReturn { - spdlog::debug("Unlinking"); + [](GstPad *tee_srcpad, GstPadProbeInfo *, gpointer user_data) -> GstPadProbeReturn { + spdlog::debug("MJPEG recording unlinking"); + + // auto pipeline = GST_PIPELINE(user_data); + auto pipeline = static_cast(user_data); + + auto queue = gst_bin_get_by_name(pipeline, "queue_record"); + if (!queue) return GST_PAD_PROBE_REMOVE; - auto pipeline = GST_PIPELINE(user_data); - auto queue = gst_bin_get_by_name(GST_BIN(pipeline), "queue_record"); auto queue_sinkpad = gst_element_get_static_pad(queue, "sink"); + if (!queue_sinkpad) { + gst_object_unref(queue); + return GST_PAD_PROBE_REMOVE; + } gst_pad_unlink(tee_srcpad, queue_sinkpad); gst_pad_send_event(queue_sinkpad, gst_event_new_eos()); @@ -466,181 +255,9 @@ bool stop_jpeg_recording(GstPipeline *pipeline) nullptr ); - gst_object_unref(tee); gst_object_unref(tee_srcpad); + gst_object_unref(tee); return true; } -void parse_video_save_binary_h265(const std::string &video_filepath) -{ - auto bin_file_name = video_filepath; - bin_file_name.replace(bin_file_name.end() - 3, bin_file_name.end(), "bin"); - - spdlog::info("parse_video_save_binary: {}", bin_file_name); - - KeyValueStore bin_store(bin_file_name); - - bin_store.openFile(); - - auto pipeline_str = std::format( - "filesrc location=\"{}\" ! matroskademux ! h265parse name=h265parse ! video/x-h265, " - "stream-format=byte-stream, alignment=au ! fakesink", - video_filepath - ); - - spdlog::info("pipeline_str = {}", pipeline_str); - - GError *error = nullptr; - std::unique_ptr pipeline( - gst_parse_launch(pipeline_str.c_str(), &error), gst_object_unref - ); - if (!pipeline) { - spdlog::error("Failed to create pipeline: {}", error->message); - g_clear_error(&error); - return; - } - - std::unique_ptr h265parse( - gst_bin_get_by_name(GST_BIN(pipeline.get()), "h265parse"), gst_object_unref - ); - std::unique_ptr src_pad{ - gst_element_get_static_pad(h265parse.get(), "src"), gst_object_unref - }; - - gst_pad_add_probe( - src_pad.get(), GST_PAD_PROBE_TYPE_BUFFER, h265_parse_saving_metadata, &bin_store, nullptr - ); - - gst_element_set_state(pipeline.get(), GST_STATE_PLAYING); - - // Event loop to keep the pipeline running - std::unique_ptr bus = { - gst_element_get_bus(pipeline.get()), gst_object_unref - }; - bool terminate = false; - - while (!terminate) { - // Wait for a message for up to 100 milliseconds - std::unique_ptr msg( - gst_bus_timed_pop_filtered( - bus.get(), - 100 * GST_MSECOND, - static_cast(GST_MESSAGE_ERROR | GST_MESSAGE_EOS) - ), - gst_message_unref - ); - - // Handle errors and EOS messages - if (msg) { - GError *err; - gchar *debug_info; - - switch (GST_MESSAGE_TYPE(msg.get())) { - case GST_MESSAGE_ERROR: - gst_message_parse_error(msg.get(), &err, &debug_info); - spdlog::error("Error from element {}:", GST_OBJECT_NAME(msg->src), err->message); - g_clear_error(&err); - g_free(debug_info); - terminate = true; - break; - case GST_MESSAGE_EOS: - spdlog::info("End-Of-Stream reached."); - terminate = true; - break; - default: break; - } - } - } - - gst_element_set_state(pipeline.get(), GST_STATE_NULL); - - bin_store.closeFile(); -} - -void parse_video_save_binary_jpeg(const std::string &video_filepath) -{ - auto bin_file_name = video_filepath; - bin_file_name.replace(bin_file_name.end() - 3, bin_file_name.end(), "bin"); - - spdlog::info("parse_video_save_binary: {}", bin_file_name); - - KeyValueStore bin_store(bin_file_name); - - bin_store.openFile(); - - auto pipeline_str = std::format( - "filesrc location=\"{}\" ! matroskademux ! jpegparse name=jpegparse ! fakesink", - video_filepath - ); - - spdlog::info("pipeline_str = {}", pipeline_str); - - GError *error = nullptr; - std::unique_ptr pipeline( - gst_parse_launch(pipeline_str.c_str(), &error), gst_object_unref - ); - - if (!pipeline) { - spdlog::error("Failed to create pipeline: {}", error->message); - g_clear_error(&error); - return; - } - - std::unique_ptr jpegparse{ - gst_bin_get_by_name(GST_BIN(pipeline.get()), "jpegparse"), gst_object_unref - }; - std::unique_ptr src_pad{ - gst_element_get_static_pad(jpegparse.get(), "src"), gst_object_unref - }; - - gst_pad_add_probe( - src_pad.get(), GST_PAD_PROBE_TYPE_BUFFER, jpeg_parse_saving_metadata, &bin_store, nullptr - ); - - gst_element_set_state(pipeline.get(), GST_STATE_PLAYING); - - // Event loop to keep the pipeline running - std::unique_ptr bus = { - gst_element_get_bus(pipeline.get()), gst_object_unref - }; - bool terminate = false; - - while (!terminate) { - // Wait for a message for up to 100 milliseconds - std::unique_ptr msg( - gst_bus_timed_pop_filtered( - bus.get(), - 100 * GST_MSECOND, - static_cast(GST_MESSAGE_ERROR | GST_MESSAGE_EOS) - ), - gst_message_unref - ); - - // Handle errors and EOS messages - if (msg) { - GError *err; - gchar *debug_info; - - switch (GST_MESSAGE_TYPE(msg.get())) { - case GST_MESSAGE_ERROR: - gst_message_parse_error(msg.get(), &err, &debug_info); - spdlog::error("Error from element {}:", GST_OBJECT_NAME(msg->src), err->message); - g_clear_error(&err); - g_free(debug_info); - terminate = true; - break; - case GST_MESSAGE_EOS: - spdlog::info("End-Of-Stream reached."); - terminate = true; - break; - default: break; - } - } - } - - gst_element_set_state(pipeline.get(), GST_STATE_NULL); - - bin_store.closeFile(); -} - } // namespace xvc \ No newline at end of file diff --git a/xdaqvc/xvc.h b/xdaqvc/xvc.h index 3ec3994..79f6eac 100644 --- a/xdaqvc/xvc.h +++ b/xdaqvc/xvc.h @@ -23,19 +23,10 @@ struct RecordConfig { } }; -void setup_h265_srt_stream(GstPipeline *pipeline, const std::string &uri); +// TODO void setup_jpeg_srt_stream(GstPipeline *pipeline, const std::string &uri); -void start_h265_recording( - GstPipeline *pipeline, std::filesystem::path &filepath, bool continuous, int max_size_time, - int max_files -); -void stop_h265_recording(GstPipeline *pipeline); - bool start_jpeg_recording(GstPipeline *, const RecordConfig &); bool stop_jpeg_recording(GstPipeline *); -void parse_video_save_binary_h265(const std::string &filepath); -void parse_video_save_binary_jpeg(const std::string &filepath); - } // namespace xvc \ No newline at end of file