From b7fb54e2bc0b3d189767d50bad9736143f3a158e Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Fri, 3 Jul 2026 09:58:06 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20clean=20up=20ndiout=20=E2=80=94=20f?= =?UTF-8?q?ix=20hardcoded=201920,=20consistent=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix: c * 1920 → c * samples_per_frame (broke non-25fps or non-48kHz). Fix: audio channel loop now iterates `channels` not `audio_slices.count`. Rename: mxl_stride → video_stride, no_samples → samples_per_frame, ndi_frame_10bit/16bit → v210_frame/p216_frame, ndi_audio_frame → ndi_audio. Scope `ast` locally to its use block. Drop unused #include "V210.hpp" and #include "FlowDef.hpp". ndi_audio struct zero-initialized then filled only when has_audio. Co-Authored-By: Claude Sonnet 4.6 --- nodes/ndiout/main.cpp | 193 +++++++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 97 deletions(-) diff --git a/nodes/ndiout/main.cpp b/nodes/ndiout/main.cpp index 19163f2..b1629be 100644 --- a/nodes/ndiout/main.cpp +++ b/nodes/ndiout/main.cpp @@ -6,8 +6,6 @@ #include #include #include "NodeBase.hpp" -#include "FlowDef.hpp" -#include "V210.hpp" #include // RAII wrapper: init NDI, create sender, destroy both on scope exit. @@ -35,6 +33,7 @@ struct NDIContext { class NDIOutNode : public dmf::NodeBase { void run() override { + // --- video flow --- const auto flow_info = config().at("flow_id"); const auto flow_id = flow_info.at("id").get(); const int width = flow_info.value("width", 1920); @@ -42,7 +41,7 @@ class NDIOutNode : public dmf::NodeBase { const int fps_num = flow_info.value("fps_num", 25); const int fps_den = flow_info.value("fps_den", 1); - log("flow=%s", flow_id.c_str()); + log("flow=%s %dx%d @ %d/%d fps", flow_id.c_str(), width, height, fps_num, fps_den); log("waiting for flow to become active..."); bool active = false; @@ -53,119 +52,114 @@ class NDIOutNode : public dmf::NodeBase { if (!dmf::g_running) return; log("flow active — starting read"); - mxlFlowReader video_reader{}; - mxlStatus vst = mxlCreateFlowReader(instance(), flow_id.c_str(), nullptr, &video_reader); - if (vst != MXL_STATUS_OK) { - log("mxlCreateFlowReader failed (status=%d)", vst); - return; - } + mxlFlowReader video_reader{}; mxlFlowConfigInfo video_cfg{}; + mxlStatus vst = mxlCreateFlowReader(instance(), flow_id.c_str(), nullptr, &video_reader); + if (vst != MXL_STATUS_OK) { log("mxlCreateFlowReader failed (status=%d)", vst); return; } mxlFlowReaderGetConfigInfo(video_reader, &video_cfg); - const uint32_t mxl_stride = video_cfg.discrete.sliceSizes[0]; + const uint32_t video_stride = video_cfg.discrete.sliceSizes[0]; - NDIContext ndi(flow_id.c_str()); - - // V210 (10-bit) intermediate and P216 (16-bit) send buffers - std::vector buf_10bit(mxl_stride * height); - std::vector buf_16bit(width * sizeof(uint16_t) * 2 * height); - - NDIlib_video_frame_v2_t ndi_frame_10bit{}; - ndi_frame_10bit.xres = width; - ndi_frame_10bit.yres = height; - ndi_frame_10bit.frame_rate_N = fps_num; - ndi_frame_10bit.frame_rate_D = fps_den; - ndi_frame_10bit.FourCC = static_cast(NDI_LIB_FOURCC('V','2','1','0')); - ndi_frame_10bit.line_stride_in_bytes = mxl_stride; - ndi_frame_10bit.p_data = buf_10bit.data(); - - NDIlib_video_frame_v2_t ndi_frame_16bit{}; - ndi_frame_16bit.xres = width; - ndi_frame_16bit.yres = height; - ndi_frame_16bit.frame_rate_N = fps_num; - ndi_frame_16bit.frame_rate_D = fps_den; - ndi_frame_16bit.line_stride_in_bytes = width * static_cast(sizeof(uint16_t)); - ndi_frame_16bit.p_data = buf_16bit.data(); - - // audio - mxlFlowReader audio_reader{}; + // --- audio flow (optional) --- + mxlFlowReader audio_reader{}; mxlFlowConfigInfo audio_cfg{}; - mxlStatus ast; - - int sample_rate = 0; - int channels = 0; - int bit_depth = 32; - int no_samples = 0; - bool has_audio = config().contains("audio_flow_id"); + int sample_rate = 0; + int channels = 0; + int samples_per_frame = 0; + bool has_audio = config().contains("audio_flow_id"); - if (has_audio) - { + if (has_audio) { const auto audio_flow_info = config().at("audio_flow_id"); - const auto audio_flow_id = audio_flow_info.at("id").get(); - ast = mxlCreateFlowReader(instance(), audio_flow_id.c_str(), nullptr, &audio_reader); + const auto audio_flow_id = audio_flow_info.at("id").get(); + sample_rate = audio_flow_info.value("sample_rate", 48000); + channels = audio_flow_info.value("channels", 2); + samples_per_frame = sample_rate / fps_num; + + log("audio flow=%s %d Hz %dch %d samples/frame", + audio_flow_id.c_str(), sample_rate, channels, samples_per_frame); + + mxlStatus ast = mxlCreateFlowReader(instance(), audio_flow_id.c_str(), nullptr, &audio_reader); if (ast != MXL_STATUS_OK) { log("audio mxlCreateFlowReader failed (status=%d) — continuing without audio", ast); has_audio = false; } else { mxlFlowReaderGetConfigInfo(audio_reader, &audio_cfg); - sample_rate = audio_flow_info.at("sample_rate").get(); - channels = audio_cfg.continuous.channelCount; - no_samples = sample_rate / fps_num; - log("audio channels: %i, samples: %i", channels, audio_cfg.continuous.bufferLength); + log("audio channels=%u buffer=%u samples", + audio_cfg.continuous.channelCount, audio_cfg.continuous.bufferLength); } } - NDIlib_audio_frame_v3_t ndi_audio_frame; - ndi_audio_frame.sample_rate = sample_rate; - ndi_audio_frame.no_channels = channels; - ndi_audio_frame.no_samples = no_samples; - ndi_audio_frame.FourCC = NDIlib_FourCC_audio_type_FLTP; - ndi_audio_frame.channel_stride_in_bytes = ndi_audio_frame.no_samples * sizeof(float); - - // core loop + NDIContext ndi(flow_id.c_str()); + + // V210 intermediate and P216 send buffers for NDI video + std::vector v210_buf(video_stride * height); + std::vector p216_buf(width * sizeof(uint16_t) * 2 * height); + + NDIlib_video_frame_v2_t v210_frame{}; + v210_frame.xres = width; + v210_frame.yres = height; + v210_frame.frame_rate_N = fps_num; + v210_frame.frame_rate_D = fps_den; + v210_frame.FourCC = static_cast(NDI_LIB_FOURCC('V','2','1','0')); + v210_frame.line_stride_in_bytes = video_stride; + v210_frame.p_data = v210_buf.data(); + + NDIlib_video_frame_v2_t p216_frame{}; + p216_frame.xres = width; + p216_frame.yres = height; + p216_frame.frame_rate_N = fps_num; + p216_frame.frame_rate_D = fps_den; + p216_frame.line_stride_in_bytes = width * static_cast(sizeof(uint16_t)); + p216_frame.p_data = p216_buf.data(); + + // Float planar buffer for NDI audio (ch0 samples, ch1 samples, ...) + std::vector audio_planar(static_cast(channels) * samples_per_frame); + NDIlib_audio_frame_v3_t ndi_audio{}; + if (has_audio) { + ndi_audio.sample_rate = sample_rate; + ndi_audio.no_channels = channels; + ndi_audio.no_samples = samples_per_frame; + ndi_audio.FourCC = NDIlib_FourCC_audio_type_FLTP; + ndi_audio.channel_stride_in_bytes = samples_per_frame * sizeof(float); + ndi_audio.p_data = reinterpret_cast(audio_planar.data()); + } + + // --- main loop --- const mxlRational video_rate = {fps_num, fps_den}; const mxlRational audio_rate = {sample_rate, 1}; - uint64_t video_index = mxlGetCurrentIndex(&video_rate); - uint64_t audio_index = has_audio ? mxlGetCurrentIndex(&audio_rate) : 0; - - uint64_t frame_count = 0; - uint64_t invalid_count = 0; - uint64_t late_count = 0; + uint64_t video_index = mxlGetCurrentIndex(&video_rate); + uint64_t audio_index = has_audio ? mxlGetCurrentIndex(&audio_rate) : 0; + uint64_t frame_count = 0; + uint64_t invalid_count = 0; + uint64_t late_count = 0; uint64_t ndi_frame_count = 0; - auto wall_start = std::chrono::steady_clock::now(); - auto last_log_time = wall_start; - - std::vector audio_planar(static_cast(channels) * no_samples); - ndi_audio_frame.p_data = reinterpret_cast(audio_planar.data()); + auto wall_start = std::chrono::steady_clock::now(); + auto last_log_time = wall_start; while (dmf::g_running.load(std::memory_order_relaxed)) { - mxlGrainInfo video_grain{}; - mxlGrainInfo audio_grain{}; - uint8_t* video_buf = nullptr; - mxlWrappedMultiBufferSlice audio_slices; - - vst = mxlFlowReaderGetGrainNonBlocking(video_reader, video_index, &video_grain, &video_buf); - bool ndi_has_connections = NDIlib_send_get_no_connections(ndi.sender, 0) > 0; - + // --- audio: non-blocking, one chunk per video frame --- if (has_audio) { - ast = mxlFlowReaderGetSamplesNonBlocking(audio_reader, audio_index, no_samples, &audio_slices); + mxlWrappedMultiBufferSlice audio_slices{}; + mxlStatus ast = mxlFlowReaderGetSamplesNonBlocking( + audio_reader, audio_index, samples_per_frame, &audio_slices); if (ast == MXL_STATUS_OK) { - for (size_t c = 0; c < audio_slices.count; c++) { - float* dst = audio_planar.data() + c * 1920; - size_t frag0_samples = audio_slices.base.fragments[0].size / sizeof(float); - const uint8_t* src0 = static_cast(audio_slices.base.fragments[0].pointer) - + c * audio_slices.stride; - std::memcpy(dst, src0, frag0_samples * sizeof(float)); - - if (audio_slices.base.fragments[1].size > 0) { - size_t frag1_samples = audio_slices.base.fragments[1].size / sizeof(float); - const uint8_t* src1 = static_cast(audio_slices.base.fragments[1].pointer) - + c * audio_slices.stride; - std::memcpy(dst + frag0_samples, src1, frag1_samples * sizeof(float)); + const size_t frag0 = audio_slices.base.fragments[0].size / sizeof(float); + const size_t frag1 = audio_slices.base.fragments[1].size / sizeof(float); + for (int c = 0; c < channels; ++c) { + float* dst = audio_planar.data() + c * samples_per_frame; + const auto* src0 = reinterpret_cast( + static_cast(audio_slices.base.fragments[0].pointer) + + c * audio_slices.stride); + std::memcpy(dst, src0, frag0 * sizeof(float)); + if (frag1 > 0) { + const auto* src1 = reinterpret_cast( + static_cast(audio_slices.base.fragments[1].pointer) + + c * audio_slices.stride); + std::memcpy(dst + frag0, src1, frag1 * sizeof(float)); } } - NDIlib_send_send_audio_v3(ndi.sender, &ndi_audio_frame); - audio_index += no_samples; + NDIlib_send_send_audio_v3(ndi.sender, &ndi_audio); + audio_index += samples_per_frame; } else if (ast == MXL_ERR_OUT_OF_RANGE_TOO_LATE) { mxlFlowRuntimeInfo ari{}; mxlFlowReaderGetRuntimeInfo(audio_reader, &ari); @@ -173,16 +167,21 @@ class NDIOutNode : public dmf::NodeBase { } } + // --- video --- + mxlGrainInfo video_grain{}; + uint8_t* video_buf = nullptr; + vst = mxlFlowReaderGetGrainNonBlocking(video_reader, video_index, &video_grain, &video_buf); + if (vst == MXL_STATUS_OK) { frame_count++; if (video_grain.flags & MXL_GRAIN_FLAG_INVALID) invalid_count++; - if (ndi_has_connections) { - std::memcpy(ndi_frame_10bit.p_data, video_buf, mxl_stride * height); - NDIlib_util_V210_to_P216(&ndi_frame_10bit, &ndi_frame_16bit); - NDIlib_send_send_video_v2(ndi.sender, &ndi_frame_16bit); + if (NDIlib_send_get_no_connections(ndi.sender, 0) > 0) { + std::memcpy(v210_frame.p_data, video_buf, video_stride * height); + NDIlib_util_V210_to_P216(&v210_frame, &p216_frame); + NDIlib_send_send_video_v2(ndi.sender, &p216_frame); if (++ndi_frame_count == 1) - log("NDI receiver connected"); + log("first NDI receiver connected"); } else { ndi_frame_count = 0; }