diff --git a/nodes/ndiin/CMakeLists.txt b/nodes/ndiin/CMakeLists.txt index c449c31..e311fa6 100644 --- a/nodes/ndiin/CMakeLists.txt +++ b/nodes/ndiin/CMakeLists.txt @@ -7,5 +7,10 @@ set(NDI_INCLUDE "${NDI_SDK_DIR}/include") target_include_directories(dmf-node-ndiin PRIVATE "${NDI_INCLUDE}" ) -find_library(NDI_LIB NAMES ndi PATHS "${NDI_SDK_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu" "${NDI_SDK_DIR}/lib/x64" "${NDI_SDK_DIR}/lib" NO_DEFAULT_PATH) +find_library(NDI_LIB NAMES ndi PATHS + "${NDI_SDK_DIR}/lib/macOS" + "${NDI_SDK_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu" + "${NDI_SDK_DIR}/lib/x64" + "${NDI_SDK_DIR}/lib" + NO_DEFAULT_PATH) target_link_libraries(dmf-node-ndiin PRIVATE ${NDI_LIB}) diff --git a/nodes/ndiin/main.cpp b/nodes/ndiin/main.cpp index ab14cb4..8a0138b 100644 --- a/nodes/ndiin/main.cpp +++ b/nodes/ndiin/main.cpp @@ -96,40 +96,49 @@ class NDIInNode : public dmf::NodeBase { dmf::NDIReceiver::AudioInfo audio_info; while (dmf::g_running.load(std::memory_order_relaxed)) { - dmf::NDIReceiver::FrameKind kind; + // Drain all queued NDI frames before writing one MXL grain. + // With audio enabled, many audio packets may be queued ahead of a video frame. + // Calling capture() once per grain window would starve video reception. + // Strategy: blocking first call (5ms), then non-blocking drain of remainder. try { - kind = ndi.capture(latest_video.data(), video_stride, audio_buf, audio_info, has_audio); + bool first = true; + while (dmf::g_running.load(std::memory_order_relaxed)) { + auto kind = ndi.capture(latest_video.data(), video_stride, + audio_buf, audio_info, has_audio, + first ? 5 : 0); + first = false; + if (kind == dmf::NDIReceiver::FrameKind::Video) { + have_video = true; + } else if (kind == dmf::NDIReceiver::FrameKind::Audio && has_audio) { + mxlMutableWrappedMultiBufferSlice slice{}; + if (mxlFlowWriterOpenSamples(audio_writer, audio_index, + static_cast(audio_info.samples), + &slice) == MXL_STATUS_OK) { + const size_t frag0 = slice.base.fragments[0].size / sizeof(float); + const size_t frag1 = slice.base.fragments[1].size / sizeof(float); + for (int c = 0; c < audio_info.channels; ++c) { + const float* src = &audio_buf[c * audio_info.channel_stride]; + auto* dst0 = reinterpret_cast( + static_cast(slice.base.fragments[0].pointer) + c * slice.stride); + std::memcpy(dst0, src, frag0 * sizeof(float)); + if (frag1 > 0) { + auto* dst1 = reinterpret_cast( + static_cast(slice.base.fragments[1].pointer) + c * slice.stride); + std::memcpy(dst1, src + frag0, frag1 * sizeof(float)); + } + } + mxlFlowWriterCommitSamples(audio_writer); + } + audio_index += audio_info.samples; + } else { + break; // None — queue drained + } + } } catch (const std::runtime_error& e) { log("NDI error: %s — stopping", e.what()); break; } - if (kind == dmf::NDIReceiver::FrameKind::Video) { - have_video = true; - } else if (kind == dmf::NDIReceiver::FrameKind::Audio && has_audio) { - mxlMutableWrappedMultiBufferSlice slice{}; - if (mxlFlowWriterOpenSamples(audio_writer, audio_index, - static_cast(audio_info.samples), &slice) == MXL_STATUS_OK) { - // MXL audio is float32 planar: each channel occupies its own ring buffer - // region, accessed at base + c * stride. Fragments handle ring wraparound. - const size_t frag0 = slice.base.fragments[0].size / sizeof(float); - const size_t frag1 = slice.base.fragments[1].size / sizeof(float); - for (int c = 0; c < audio_info.channels; ++c) { - const float* src = &audio_buf[c * audio_info.channel_stride]; - auto* dst0 = reinterpret_cast( - static_cast(slice.base.fragments[0].pointer) + c * slice.stride); - std::memcpy(dst0, src, frag0 * sizeof(float)); - if (frag1 > 0) { - auto* dst1 = reinterpret_cast( - static_cast(slice.base.fragments[1].pointer) + c * slice.stride); - std::memcpy(dst1, src + frag0, frag1 * sizeof(float)); - } - } - mxlFlowWriterCommitSamples(audio_writer); - } - audio_index += audio_info.samples; - } - // Write video grain whenever the MXL clock has reached video_index const uint64_t current = mxlGetCurrentIndex(&video_rate); if (current >= video_index) { diff --git a/nodes/ndiout/CMakeLists.txt b/nodes/ndiout/CMakeLists.txt index a54c2b6..fd69a00 100644 --- a/nodes/ndiout/CMakeLists.txt +++ b/nodes/ndiout/CMakeLists.txt @@ -7,5 +7,10 @@ set(NDI_INCLUDE "${NDI_SDK_DIR}/include") target_include_directories(dmf-node-ndiout PRIVATE "${NDI_INCLUDE}" ) -find_library(NDI_LIB NAMES ndi PATHS "${NDI_SDK_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu" "${NDI_SDK_DIR}/lib/x64" "${NDI_SDK_DIR}/lib" NO_DEFAULT_PATH) +find_library(NDI_LIB NAMES ndi PATHS + "${NDI_SDK_DIR}/lib/macOS" + "${NDI_SDK_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu" + "${NDI_SDK_DIR}/lib/x64" + "${NDI_SDK_DIR}/lib" + NO_DEFAULT_PATH) target_link_libraries(dmf-node-ndiout PRIVATE ${NDI_LIB}) diff --git a/shared/NDIReceiver.hpp b/shared/NDIReceiver.hpp index 7ef8935..989fb3e 100644 --- a/shared/NDIReceiver.hpp +++ b/shared/NDIReceiver.hpp @@ -124,15 +124,16 @@ public: // Receives one NDI frame. On video: converts to V210 in frame_buffer/frame_stride. // On audio: copies float32 planar samples into audio_out and fills audio_info. // Returns FrameKind::None on timeout or non-A/V frames. - // Throws on source lost or video format change. + // Throws on source lost or unsupported video format. + // timeout_ms=0 is non-blocking (useful for draining queued frames after an initial wait). FrameKind capture(uint8_t* frame_buffer, uint32_t frame_stride, std::vector& audio_out, AudioInfo& audio_info, - bool want_audio = false) { + bool want_audio = false, int timeout_ms = 5) { NDIlib_video_frame_v2_t video_frame{}; NDIlib_audio_frame_v3_t audio_frame{}; auto type = NDIlib_recv_capture_v3(recv_, &video_frame, want_audio ? &audio_frame : nullptr, - nullptr, 5); + nullptr, timeout_ms); if (type == NDIlib_frame_type_error) throw std::runtime_error("NDI source lost");