#pragma once #include #include #include #include #include "Signal.hpp" #include "V210.hpp" namespace dmf { class NDIReceiver { public: struct SourceInfo { int width = 0; int height = 0; int fps_num = 0; int fps_den = 0; int stride = 0; NDIlib_FourCC_type_e fourcc{}; }; NDIReceiver() { if (!NDIlib_is_supported_CPU()) throw std::runtime_error("CPU is not sufficient for NDI"); if (!NDIlib_initialize()) throw std::runtime_error("NDI lib init failed"); } ~NDIReceiver() { if (recv_) NDIlib_recv_destroy(recv_); if (find_) NDIlib_find_destroy(find_); NDIlib_destroy(); } // Discovers NDI sources on the network. Polls up to max_attempts times, // each waiting timeout_ms milliseconds. Respects g_running. std::vector find_sources(uint32_t timeout_ms, int max_attempts = 10) { find_ = NDIlib_find_create_v2(); if (!find_) throw std::runtime_error("Cannot create NDI finder"); uint32_t count = 0; const NDIlib_source_t* p_sources = nullptr; for (int i = 0; !count && i < max_attempts; ++i) { if (!dmf::g_running.load(std::memory_order_relaxed)) { NDIlib_find_destroy(find_); find_ = nullptr; throw std::runtime_error("Interrupted while searching for NDI sources"); } NDIlib_find_wait_for_sources(find_, timeout_ms); p_sources = NDIlib_find_get_current_sources(find_, &count); } if (!count) { NDIlib_find_destroy(find_); find_ = nullptr; throw std::runtime_error("No NDI sources found after timeout"); } // Copy while finder is alive: p_sources points into finder-owned memory std::vector names; for (uint32_t i = 0; i < count; ++i) { names.emplace_back(p_sources[i].p_ndi_name); source_names_.emplace_back(p_sources[i].p_ndi_name); source_urls_.emplace_back(p_sources[i].p_url_address); } sources_.reserve(source_names_.size()); for (size_t i = 0; i < source_names_.size(); ++i) sources_.push_back({source_names_[i].c_str(), source_urls_[i].c_str()}); NDIlib_find_destroy(find_); find_ = nullptr; return names; } // Connects to a discovered source by index. void connect(uint32_t source_num) { if (sources_.empty()) throw std::runtime_error("No sources available — call find_sources() first"); if (source_num >= static_cast(sources_.size())) throw std::runtime_error("source_num exceeds available source count"); recv_ = NDIlib_recv_create_v3(); if (!recv_) throw std::runtime_error("Cannot create NDI receive instance"); NDIlib_recv_connect(recv_, &sources_[source_num]); } // Captures the first video frame to determine resolution, frame rate, and format. // Stores the result internally for use by capture_v210(). Respects g_running. SourceInfo probe() { while (dmf::g_running.load(std::memory_order_relaxed)) { NDIlib_video_frame_v2_t frame; auto type = NDIlib_recv_capture_v3(recv_, &frame, nullptr, nullptr, 1000); if (type == NDIlib_frame_type_error) throw std::runtime_error("NDI source lost during probe"); if (type != NDIlib_frame_type_video) continue; info_.width = frame.xres; info_.height = frame.yres; info_.fps_num = frame.frame_rate_N; info_.fps_den = frame.frame_rate_D; info_.fourcc = static_cast(frame.FourCC); info_.stride = frame.line_stride_in_bytes; if (info_.stride == 0) info_.stride = info_.width * bytes_per_pixel(info_.fourcc); NDIlib_recv_free_video_v2(recv_, &frame); return info_; } throw std::runtime_error("Interrupted during probe"); } // Captures one video frame and converts it to V210 in frame_buffer. // Returns false if no frame was available this tick (caller should repeat last frame). // Throws on source lost or unsupported format. bool capture_v210(uint8_t* frame_buffer, uint32_t frame_stride) { NDIlib_video_frame_v2_t frame; auto type = NDIlib_recv_capture_v3(recv_, &frame, nullptr, nullptr, 5); if (type == NDIlib_frame_type_error) throw std::runtime_error("NDI source lost"); if (type == NDIlib_frame_type_status_change) { info_ = probe(); // source changed resolution or framerate — refresh return false; } if (type != NDIlib_frame_type_video) return false; switch (info_.fourcc) { case NDIlib_FourCC_type_UYVY: v210::UYVYtoV210(frame.p_data, frame_buffer, info_.width, info_.height, info_.stride, frame_stride); break; case NDIlib_FourCC_type_P216: { NDIlib_video_frame_v2_t dst{}; dst.p_data = frame_buffer; dst.line_stride_in_bytes = frame_stride; NDIlib_util_P216_to_V210(&frame, &dst); break; } default: NDIlib_recv_free_video_v2(recv_, &frame); throw std::runtime_error("Unsupported NDI color format: " + fourcc_str(info_.fourcc)); } NDIlib_recv_free_video_v2(recv_, &frame); return true; } private: NDIlib_find_instance_t find_ = nullptr; NDIlib_recv_instance_t recv_ = nullptr; SourceInfo info_; std::vector source_names_; std::vector source_urls_; std::vector sources_; static int bytes_per_pixel(NDIlib_FourCC_type_e fc) { switch (fc) { case NDIlib_FourCC_video_type_UYVY: case NDIlib_FourCC_video_type_YV12: case NDIlib_FourCC_video_type_I420: case NDIlib_FourCC_video_type_NV12: return 2; case NDIlib_FourCC_video_type_BGRA: case NDIlib_FourCC_video_type_RGBA: case NDIlib_FourCC_video_type_BGRX: case NDIlib_FourCC_video_type_RGBX: return 4; case NDIlib_FourCC_video_type_UYVA: return 3; case NDIlib_FourCC_video_type_P216: return 4; case NDIlib_FourCC_video_type_PA16: return 6; default: return 2; } } static std::string fourcc_str(NDIlib_FourCC_type_e fc) { uint32_t v = static_cast(fc); char s[5] = { static_cast((v >> 0) & 0xFF), static_cast((v >> 8) & 0xFF), static_cast((v >> 16) & 0xFF), static_cast((v >> 24) & 0xFF), '\0' }; return s; } }; } // namespace dmf