From 7a32fa20af099953d5d4e721f2e68021e5387058 Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Wed, 1 Jul 2026 12:07:45 +0300 Subject: [PATCH] fix: NDI node correctness and robustness fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NDIHelper: - find_sources: check g_running each attempt to avoid 50s block on shutdown - getV210_video_frame: fix P216 branch (fall-through + local pointer reassignment bug) - getV210_video_frame: status_change re-queries source info instead of throwing - select_source: fix typo "recieve" → "receive" ndiout: - set frame_rate_N/frame_rate_D on the NDI send frame (was 0/0) - add per-second stats logging (matching fakesink pattern) - add missing include - fix typo "reciever" → "receiver" ndiin: - read source_num from NODE_CONFIG (key: "source_num", default 0) - wrap hot-loop NDI call in try/catch so source-lost terminates cleanly - pass source_num through to getV210_video_frame Co-Authored-By: Claude Sonnet 4.6 --- nodes/ndiin/main.cpp | 16 ++++++++++------ nodes/ndiout/main.cpp | 34 +++++++++++++++++++++------------ shared/NDIHelper.hpp | 44 +++++++++++++++++++++++++++---------------- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/nodes/ndiin/main.cpp b/nodes/ndiin/main.cpp index c33f691..fd106c8 100644 --- a/nodes/ndiin/main.cpp +++ b/nodes/ndiin/main.cpp @@ -10,15 +10,15 @@ class NDIInNode : public dmf::NodeBase { void run() override { + const uint32_t source_num = config().value("source_num", 0); + dmf::NDIHelper ndi_helper; try { std::vector ndi_sources; ndi_helper.find_sources(&ndi_sources, 5000); log("Available NDI sources:"); - for (const auto& source_name : ndi_sources){ - log("%s", source_name.c_str()); - } - uint32_t source_num = 0; + for (const auto& source_name : ndi_sources) + log(" %s", source_name.c_str()); ndi_helper.select_source(source_num); ndi_helper.get_source_info(source_num); } catch (const std::runtime_error& e) { @@ -63,8 +63,12 @@ class NDIInNode : public dmf::NodeBase { bool last_ndi_frame_valid = false; while (dmf::g_running.load(std::memory_order_relaxed)) { - if (ndi_helper.getV210_video_frame(0, latest_buffer, stride)) { - last_ndi_frame_valid = true; + try { + if (ndi_helper.getV210_video_frame(source_num, latest_buffer, stride)) + last_ndi_frame_valid = true; + } catch (const std::runtime_error& e) { + log("NDI error: %s — stopping", e.what()); + break; } uint8_t* buf = nullptr; diff --git a/nodes/ndiout/main.cpp b/nodes/ndiout/main.cpp index 72a83eb..51fb3d7 100644 --- a/nodes/ndiout/main.cpp +++ b/nodes/ndiout/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -38,12 +39,12 @@ class NDIOutNode : public dmf::NodeBase { const mxlRational rate = {fps_num, fps_den}; - uint64_t index = mxlGetCurrentIndex(&rate); - uint64_t frame_count = 0; - uint64_t invalid_count = 0; - uint64_t late_count = 0; - auto wall_start = std::chrono::steady_clock::now(); - auto last_log_time = wall_start; + uint64_t index = mxlGetCurrentIndex(&rate); + uint64_t frame_count = 0; + uint64_t invalid_count = 0; + uint64_t late_count = 0; + auto wall_start = std::chrono::steady_clock::now(); + auto last_log_time = wall_start; // NDI part if (!NDIlib_initialize()) { @@ -73,9 +74,10 @@ class NDIOutNode : public dmf::NodeBase { NDI_video_frame_10bit.p_data = (uint8_t*)malloc(NDI_video_frame_10bit.line_stride_in_bytes * NDI_video_frame_10bit.yres); NDIlib_video_frame_v2_t NDI_video_frame_16bit; - NDI_video_frame_16bit.xres = NDI_video_frame_10bit.xres; - NDI_video_frame_16bit.yres = NDI_video_frame_10bit.yres; - + NDI_video_frame_16bit.xres = NDI_video_frame_10bit.xres; + NDI_video_frame_16bit.yres = NDI_video_frame_10bit.yres; + NDI_video_frame_16bit.frame_rate_N = fps_num; + NDI_video_frame_16bit.frame_rate_D = fps_den; NDI_video_frame_16bit.line_stride_in_bytes = NDI_video_frame_16bit.xres * sizeof(uint16_t); NDI_video_frame_16bit.p_data = (uint8_t*)malloc(NDI_video_frame_16bit.line_stride_in_bytes * 2 * NDI_video_frame_16bit.yres); // @@ -98,9 +100,8 @@ class NDIOutNode : public dmf::NodeBase { NDIlib_util_V210_to_P216(&NDI_video_frame_10bit, &NDI_video_frame_16bit); NDIlib_send_send_video_v2(pNDI_send, &NDI_video_frame_16bit); ndi_frame_counter++; - if (ndi_frame_counter == 1) { - log("NDI reciever got feed"); - } + if (ndi_frame_counter == 1) + log("NDI receiver connected"); } else if (st == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { mxlSleepForNs(1'000'000); // 1 ms poll @@ -114,6 +115,15 @@ class NDIOutNode : public dmf::NodeBase { log("unexpected status=%d on index=%llu", st, index); break; } + + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration(now - last_log_time).count() >= 1.0) { + const double elapsed = std::chrono::duration(now - wall_start).count(); + log("frames=%llu invalid=%llu late=%llu avg=%.2f fps", + frame_count, invalid_count, late_count, + static_cast(frame_count) / elapsed); + last_log_time = now; + } } log("stopped — total frames=%llu invalid=%llu late=%llu", diff --git a/shared/NDIHelper.hpp b/shared/NDIHelper.hpp index 6db5a89..50ca5e3 100644 --- a/shared/NDIHelper.hpp +++ b/shared/NDIHelper.hpp @@ -2,6 +2,7 @@ #include #include #include +#include "Signal.hpp" #include "V210.hpp" namespace dmf { @@ -35,6 +36,11 @@ class NDIHelper { uint32_t sources_amount = 0; const NDIlib_source_t* p_sources = nullptr; for (int attempt = 0; !sources_amount && attempt < max_attempts; ++attempt) { + if (!dmf::g_running.load(std::memory_order_relaxed)) { + NDIlib_find_destroy(pNDI_find); + pNDI_find = nullptr; + throw std::runtime_error("Interrupted while searching for NDI sources"); + } NDIlib_find_wait_for_sources(pNDI_find, timeout_ms); p_sources = NDIlib_find_get_current_sources(pNDI_find, &sources_amount); } @@ -68,7 +74,7 @@ class NDIHelper { pNDI_recv = NDIlib_recv_create_v3(); if (!pNDI_recv) { - throw std::runtime_error("Cannot create NDI recieve instance"); + throw std::runtime_error("Cannot create NDI receive instance"); } NDIlib_recv_connect(pNDI_recv, &cached_sources[source_num]); } @@ -154,28 +160,34 @@ class NDIHelper { NDIlib_video_frame_v2_t video_frame; NDIlib_frame_type_e frame_type; frame_type = NDIlib_recv_capture_v3(pNDI_recv, &video_frame, nullptr, nullptr, 5); - switch(frame_type) - { - case NDIlib_frame_type_error: - throw std::runtime_error("NDI source lost"); - case NDIlib_frame_type_status_change: - throw std::runtime_error("NDI source resolution or framerate are changed"); - } - if (frame_type != NDIlib_frame_type_video) { + if (frame_type == NDIlib_frame_type_error) + throw std::runtime_error("NDI source lost"); + + if (frame_type == NDIlib_frame_type_status_change) { + // Source changed resolution or framerate — refresh internal info, repeat last frame + get_source_info(source_num); return false; } - switch(fourCC) - { + + if (frame_type != NDIlib_frame_type_video) + return false; + + switch (fourCC) { case NDIlib_FourCC_type_UYVY: v210::UYVYtoV210(video_frame.p_data, frame_buffer, xres, yres, stride, frame_stride); break; - case NDIlib_FourCC_type_P216: - NDIlib_video_frame_v2_t video_frame_10bit; - NDIlib_util_P216_to_V210(&video_frame, &video_frame_10bit); - frame_buffer = video_frame.p_data; + case NDIlib_FourCC_type_P216: { + // P216→V210: write directly into caller's buffer via a wrapper frame + NDIlib_video_frame_v2_t dst{}; + dst.p_data = frame_buffer; + dst.line_stride_in_bytes = frame_stride; + NDIlib_util_P216_to_V210(&video_frame, &dst); + break; + } default: - throw std::runtime_error("Color format is not supported yet"); + NDIlib_recv_free_video_v2(pNDI_recv, &video_frame); + throw std::runtime_error("Unsupported NDI color format: " + fourCCtoStr()); } NDIlib_recv_free_video_v2(pNDI_recv, &video_frame); return true;