From c60c5645ed3d17b1006af77880dc503882fa3893 Mon Sep 17 00:00:00 2001 From: itten Date: Mon, 6 Jul 2026 01:27:19 +0300 Subject: [PATCH] working video setup --- .vscode/c_cpp_properties.json | 3 +- CMakeLists.txt | 6 + decklink-ndi.json | 13 ++ nodes/decklinkin/CMakeLists.txt | 11 + nodes/decklinkin/main.cpp | 123 +++++++++++ shared/DeckLinkReceiver.hpp | 374 ++++++++++++++++++++++++++++++++ 6 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 decklink-ndi.json create mode 100644 nodes/decklinkin/CMakeLists.txt create mode 100644 nodes/decklinkin/main.cpp create mode 100644 shared/DeckLinkReceiver.hpp diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 2bd9817..672b1ad 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -5,7 +5,8 @@ "includePath": [ "${workspaceFolder}/**", "${workspaceFolder}/shared", - "${HOME}/SDK/NDI/include" + "${HOME}/SDK/NDI/include", + "${HOME}/SDK/decklink-sdk/Linux/include" ], "defines": [], "compilerPath": "/usr/bin/clang", diff --git a/CMakeLists.txt b/CMakeLists.txt index 967131f..f93aadc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,12 @@ if(NDI_SDK_DIR) add_subdirectory(nodes/ndiin) endif() +# ── DeckLink nodes ──────────────────────────────────────────────────────────────── +set(DECKLINK_SDK_DIR "" CACHE PATH "Path to DeckLink SDK root") +if(DECKLINK_SDK_DIR) + add_subdirectory(nodes/decklinkin) +endif() + add_subdirectory(nodes/videoin) # ── Core server ────────────────────────────────────────────────────────────── diff --git a/decklink-ndi.json b/decklink-ndi.json new file mode 100644 index 0000000..1fdd3bf --- /dev/null +++ b/decklink-ndi.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { "id": "decklinkin", "type": "decklinkin", "params": {} }, + { "id": "ndiout", "type": "ndiout", "params": {} } + ], + "edges": [ + { + "from": "decklinkin", "from_port": "video_flow_id", + "to": "ndiout", "to_port": "video_flow_id", + "format": { "kind": "video", "width": 1920, "height": 1080, "fps_num": 25, "fps_den": 1 } + } + ] +} diff --git a/nodes/decklinkin/CMakeLists.txt b/nodes/decklinkin/CMakeLists.txt new file mode 100644 index 0000000..4f3a9b4 --- /dev/null +++ b/nodes/decklinkin/CMakeLists.txt @@ -0,0 +1,11 @@ +set(DECKLINK_INCLUDE "${DECKLINK_SDK_DIR}/include" + CACHE PATH "Path to DeckLink API headers") + +add_executable(dmf-node-decklinkin + main.cpp + "${DECKLINK_INCLUDE}/DeckLinkAPIDispatch.cpp" +) +target_compile_features(dmf-node-decklinkin PRIVATE cxx_std_20) +target_include_directories(dmf-node-decklinkin PRIVATE "${DECKLINK_INCLUDE}") +target_link_libraries(dmf-node-decklinkin PRIVATE dmf-shared ${CMAKE_DL_LIBS}) +install(TARGETS dmf-node-decklinkin RUNTIME DESTINATION bin) diff --git a/nodes/decklinkin/main.cpp b/nodes/decklinkin/main.cpp new file mode 100644 index 0000000..e04dbc1 --- /dev/null +++ b/nodes/decklinkin/main.cpp @@ -0,0 +1,123 @@ +#include "NodeBase.hpp" +#include "DeckLinkReceiver.hpp" +#include "Signal.hpp" +#include "FlowDef.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +class NodeDeckLinkIn: public dmf::NodeBase { + void run() override { + dmf::DeckLinkReceiver decklink_receiver; + log("Available DeckLink input devices:"); + for (auto device : decklink_receiver.devices_list) { + log("%i) %s", device.index, device.display_name.c_str()); + } + int selected_device = 0; + decklink_receiver.start_capture(selected_device); + log( + "DeckLink feed for device '%s':", + decklink_receiver.devices_list.at(selected_device).display_name.c_str() + ); + + dmf::SourceInfo video_source_info{}; + if (!decklink_receiver.wait_for_format(5000)) { + log("Timeout waiting for format detection"); + return; + } + video_source_info = decklink_receiver.get_input_callback()->video_info; + if (video_source_info.width == 0 || video_source_info.fps_num == 0) { + log("Invalid format detected"); + return; + } + + + const auto video_flow_info = config().at("video_flow_id"); + const auto video_flow_id = video_flow_info.at("id").get(); + const int width = video_flow_info.value("width", video_source_info.width); + const int height = video_flow_info.value("height", video_source_info.height); + const int fps_num = video_flow_info.value("fps_num", video_source_info.fps_num); + const int fps_den = video_flow_info.value("fps_den", video_source_info.fps_den); + + log("video flow=%s %dx%d @ %d/%d fps", video_flow_id.c_str(), width, height, fps_num, fps_den); + + mxlFlowWriter video_writer{}; + mxlFlowConfigInfo video_cfg{}; + bool created = false; + mxlStatus vst = mxlCreateFlowWriter( + instance(), + dmf::make_video_flow_def(video_flow_id, node_id(), width, height, fps_num, fps_den).c_str(), + "", &video_writer, &video_cfg, &created); + if (vst != MXL_STATUS_OK) { log("video mxlCreateFlowWriter failed (%s)", dmf::mxl_status_str(vst)); return; } + + const uint32_t video_stride = video_cfg.discrete.sliceSizes[0]; + log("video stride=%u B/line grain=%u B ring=%u grains", + video_stride, video_stride * static_cast(height), video_cfg.discrete.grainCount); + + const mxlRational video_rate = {fps_num, fps_den}; + uint64_t video_index = mxlGetCurrentIndex(&video_rate); + log("start video_index=%lu", static_cast(video_index)); + + auto* cb = decklink_receiver.get_input_callback(); + const uint32_t dst_row = video_stride; + std::vector local_frame(dst_row * height, 0); + + while(dmf::g_running.load(std::memory_order_relaxed)) { + // Wait for a fresh frame from the callback + uint32_t src_row; + int src_w, src_h; + { + std::unique_lock lk(cb->frame_mutex); + cb->frame_cv.wait(lk, [&] { + return cb->frame_ready.load(std::memory_order_acquire) + || !dmf::g_running.load(std::memory_order_relaxed); + }); + if (!dmf::g_running.load(std::memory_order_relaxed)) break; + src_row = cb->frame_row_bytes ? cb->frame_row_bytes : dst_row; + src_w = cb->frame_width; + src_h = cb->frame_height; + // Row-by-row copy with zero padding for stride difference + const uint8_t* src = cb->frame_buffer.data(); + uint8_t* dst = local_frame.data(); + const uint32_t copy_row = std::min(src_row, dst_row); + const int rows = std::min(src_h, height); + std::memset(local_frame.data(), 0, local_frame.size()); + for (int y = 0; y < rows; ++y) { + std::memcpy(dst, src, copy_row); + src += src_row; + dst += dst_row; + } + cb->frame_ready.store(false, std::memory_order_release); + } + if (src_w != width || src_h != height) { + log("frame dim mismatch: src=%dx%d mxl=%dx%d (skipping)", src_w, src_h, width, height); + continue; + } + // MXL write — no lock held, callback can fill next frame in parallel + mxlGrainInfo grain{}; + uint8_t* video_buf = nullptr; + vst = mxlFlowWriterOpenGrain(video_writer, video_index, &grain, &video_buf); + if (vst == MXL_STATUS_OK) { + std::memcpy(video_buf, local_frame.data(), local_frame.size()); + grain.flags = 0; + grain.validSlices = grain.totalSlices; + mxlFlowWriterCommitGrain(video_writer, &grain); + const uint64_t ns = mxlGetNsUntilIndex(video_index + 1, &video_rate); + if (ns > 0 && ns < 2'000'000'000ULL) mxlSleepForNs(ns); + video_index++; + } + } + } +}; + +int main () +{ + NodeDeckLinkIn node; + node.execute(); + return 0; +} \ No newline at end of file diff --git a/shared/DeckLinkReceiver.hpp b/shared/DeckLinkReceiver.hpp new file mode 100644 index 0000000..92b07b4 --- /dev/null +++ b/shared/DeckLinkReceiver.hpp @@ -0,0 +1,374 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dmf { class DeckLinkReceiver; } + +namespace dmf { + +struct SourceInfo { + int width = 0; + int height = 0; + int fps_num = 0; + int fps_den = 0; + int stride = 0; +}; + +class DeckLinkInputCallback: public IDeckLinkInputCallback + { + public: + bool is_video_info_set() const { return video_info_set.load(std::memory_order_acquire); } + SourceInfo video_info{}; + + std::mutex frame_mutex; + std::condition_variable frame_cv; + std::vector frame_buffer; + std::atomic frame_ready{false}; + size_t frame_size = 0; + uint32_t frame_row_bytes = 0; // actual stride from GetRowBytes() + int frame_width = 0; + int frame_height = 0; + + DeckLinkInputCallback( + dmf::DeckLinkReceiver* receiver, + bool detection_enabled, + std::mutex& mtx, + std::condition_variable& cv + ) + : decklink_receiver(receiver) + , ref_count(1) + , detection_enabled(detection_enabled) + , format_mutex(mtx) + , format_cv(cv) + {} + + void set_decklink_input(IDeckLinkInput* input) { deckLink_input = input; } + + HRESULT STDMETHODCALLTYPE VideoInputFormatChanged( + BMDVideoInputFormatChangedEvents notification_events, + IDeckLinkDisplayMode *new_display_mode, + BMDDetectedVideoInputFormatFlags detected_signal_flags + ) override + { + store_format(new_display_mode, nullptr); + long w = new_display_mode->GetWidth(); + long h = new_display_mode->GetHeight(); + BMDTimeValue dur; BMDTimeScale ts; + new_display_mode->GetFrameRate(&dur, &ts); + double fps = (double)ts / (double)dur; + const char* name = nullptr; + new_display_mode->GetName(&name); + printf("Input changed to: %ldx%ld @ %.2f fps (%s)\n", w, h, fps, name ? name : "?"); + + // Discard any stale frame from the placeholder mode + { + std::lock_guard lk(frame_mutex); + frame_ready.store(false, std::memory_order_release); + frame_width = 0; + frame_height = 0; + frame_buffer.clear(); + } + + // Re-enable video input with the detected display mode (per SDK samples) + if (deckLink_input && (notification_events & bmdVideoInputDisplayModeChanged)) { + HRESULT r = deckLink_input->StopStreams(); + if (r == S_OK) { + r = deckLink_input->EnableVideoInput( + new_display_mode->GetDisplayMode(), + bmdFormat10BitYUV, + bmdVideoInputEnableFormatDetection); + if (r == S_OK) { + r = deckLink_input->StartStreams(); + if (r != S_OK) { + printf("StartStreams after reconfig failed (0x%08x)\n", r); + } + } else { + printf("EnableVideoInput after reconfig failed (0x%08x)\n", r); + } + } else { + printf("StopStreams for reconfig failed (0x%08x)\n", r); + } + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE VideoInputFrameArrived( + IDeckLinkVideoInputFrame* video_frame, + IDeckLinkAudioInputPacket* audio_packet + ) override + { + if (!video_frame) return S_OK; + + // One-time format detection from first frame (only when auto-detection + // is OFF — otherwise VideoInputFormatChanged is authoritative). + if (!detection_enabled + && !video_info_set.load(std::memory_order_relaxed)) + { + BMDTimeValue frame_time = 0, frame_duration = 0; + video_frame->GetStreamTime(&frame_time, &frame_duration, 10000000); + { + std::lock_guard lk(video_info_mutex); + video_info.width = static_cast(video_frame->GetWidth()); + video_info.height = static_cast(video_frame->GetHeight()); + video_info.stride = static_cast(video_frame->GetRowBytes()); + if (frame_duration > 0) { + video_info.fps_num = 10000000; + video_info.fps_den = static_cast(frame_duration); + int g = std::gcd(video_info.fps_num, video_info.fps_den); + if (g > 1) { video_info.fps_num /= g; video_info.fps_den /= g; } + } + if (video_info.width > 0 && video_info.height > 0 && video_info.fps_num > 0) { + video_info_set.store(true, std::memory_order_release); + std::lock_guard flk(format_mutex); + format_cv.notify_one(); + } + } + } + + // Always capture the frame + IDeckLinkVideoBuffer* buf = nullptr; + if (video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) != S_OK) + return S_OK; + + buf->StartAccess(bmdBufferAccessRead); + void* src = nullptr; + buf->GetBytes(&src); + if (src) { + uint32_t row_bytes = static_cast(video_frame->GetRowBytes()); + int fw = static_cast(video_frame->GetWidth()); + int fh = static_cast(video_frame->GetHeight()); + size_t sz = static_cast(row_bytes) + * static_cast(fh); + std::lock_guard lk(frame_mutex); + if (frame_buffer.size() < sz) + frame_buffer.resize(sz); + std::memcpy(frame_buffer.data(), src, sz); + frame_size = sz; + frame_row_bytes = row_bytes; + frame_width = fw; + frame_height = fh; + frame_ready.store(true, std::memory_order_release); + frame_cv.notify_one(); + } + buf->EndAccess(bmdBufferAccessRead); + buf->Release(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override + { + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return ++ref_count; + } + + ULONG STDMETHODCALLTYPE Release() override + { + return --ref_count; // DeckLinkReceiver destructor owns deletion + } + + private: + dmf::DeckLinkReceiver* decklink_receiver; + IDeckLinkInput* deckLink_input = nullptr; + std::atomic ref_count; + bool detection_enabled = false; + std::mutex& format_mutex; + std::condition_variable& format_cv; + + std::atomic video_info_set{false}; + std::mutex video_info_mutex; + + void store_format(IDeckLinkDisplayMode* dm, IDeckLinkVideoInputFrame* /*vf*/) { + if (!dm) return; + std::lock_guard lk(video_info_mutex); + video_info.width = static_cast(dm->GetWidth()); + video_info.height = static_cast(dm->GetHeight()); + video_info.stride = video_info.width * 8 / 3; // V210 + BMDTimeValue dur = 0; BMDTimeScale ts = 0; + dm->GetFrameRate(&dur, &ts); + if (dur > 0 && ts > 0) { + video_info.fps_num = static_cast(ts); + video_info.fps_den = static_cast(dur); + int g = std::gcd(video_info.fps_num, video_info.fps_den); + if (g > 1) { video_info.fps_num /= g; video_info.fps_den /= g; } + } + if (video_info.width > 0 && video_info.height > 0 && video_info.fps_num > 0) { + video_info_set.store(true, std::memory_order_release); + { + std::lock_guard lk(format_mutex); + format_cv.notify_one(); + } + printf("Format from change: %dx%d @ %d/%d fps stride=%d\n", + video_info.width, video_info.height, + video_info.fps_num, video_info.fps_den, video_info.stride); + } + } +}; + +struct DeviceInfo { + uint32_t index; + IDeckLink* device; + std::string display_name; +}; + +class DeckLinkReceiver +{ + public: + SourceInfo video_source_info{}; + std::vector devices_list; + + DeckLinkReceiver() { + selected_device_index = 0; + selected_device = nullptr; + device_config = nullptr; + device_status = nullptr; + deckLink_notification = nullptr; + // notificationCallback = nullptr; + deckLink_input = nullptr; + input_callback = nullptr; + if (!get_input_device_list()) { + throw std::runtime_error("DeckLink devices not found"); + } + } + + ~DeckLinkReceiver() { + if (input_callback) { + if (deckLink_input) deckLink_input->SetCallback(nullptr); + delete input_callback; + input_callback = nullptr; + } + // if (m_notificationCallback) + // { + // m_deckLinkNotification->Unsubscribe(bmdStatusChanged, m_notificationCallback); + // m_notificationCallback->Release(); + // } + if (selected_device) selected_device->Release(); + if (device_config) device_config->Release(); + if (device_status) device_status->Release(); + if (deckLink_input) deckLink_input->Release(); + if (deckLink_notification) deckLink_notification->Release(); + } + + HRESULT start_capture(uint32_t device_index) + { + const bool detection = true; // format-detection mode + setup(device_index, detection); + + HRESULT result = deckLink_input->EnableVideoInput( + bmdModeNTSC, // placeholder; detection will override + bmdFormat10BitYUV, + bmdVideoInputEnableFormatDetection + ); + if (result != S_OK) + { + throw std::runtime_error("Could not enable video input"); + } + + result = deckLink_input->StartStreams(); + if (result != S_OK) + { + throw std::runtime_error("Could not start capture"); + } + return S_OK; + } + + DeckLinkInputCallback* get_input_callback() { + return input_callback; + } + + IDeckLinkInput* get_input() { return deckLink_input; } + + bool wait_for_format(uint64_t timeout_ms = 5000) { + std::unique_lock lk(mutex); + return signal_condition.wait_for(lk, std::chrono::milliseconds(timeout_ms), + [this] { return input_callback && input_callback->is_video_info_set(); }); + } + + private: + uint32_t selected_device_index = 0; + IDeckLink* selected_device = nullptr; + IDeckLinkConfiguration* device_config; + IDeckLinkStatus* device_status; + IDeckLinkNotification* deckLink_notification; + // NotificationCallback* m_notificationCallback; + IDeckLinkInput* deckLink_input; + DeckLinkInputCallback* input_callback; + std::mutex mutex; + std::condition_variable signal_condition; + + bool get_input_device_list() + { + IDeckLinkIterator* decklink_iterator = CreateDeckLinkIteratorInstance(); + if (!decklink_iterator) { + throw std::runtime_error("A DeckLink iterator could not be created. The DeckLink drivers may not be installed"); + return false; + } + + IDeckLink* device = nullptr; + IDeckLinkInput* input = nullptr; + uint32_t index = 0; + while (true) { + device = nullptr; + HRESULT result = decklink_iterator->Next(&device); + if (result != S_OK) break; + input = nullptr; + result = device->QueryInterface(IID_IDeckLinkInput, (void**)&input); + if (input) input->Release(); + if (result != S_OK) { device->Release(); continue; } + const char* display_name = nullptr; + device->GetDisplayName(&display_name); + devices_list.push_back({index, device, std::string(display_name ? display_name : "?")}); + index++; + } + if (decklink_iterator) decklink_iterator->Release(); + return index > 0; + } + + HRESULT setup(uint32_t device_index, bool detection_enabled) { + selected_device_index = device_index; + selected_device = devices_list.at(selected_device_index).device; + selected_device->AddRef(); + + HRESULT result = selected_device->QueryInterface(IID_IDeckLinkConfiguration, (void**)&device_config); + if (result != S_OK) { + throw std::runtime_error("Could not obtain the IDeckLinkConfiguration interface"); + } + + result = selected_device->QueryInterface(IID_IDeckLinkStatus, (void**)&device_status); + if (result != S_OK) { + throw std::runtime_error("Could not obtain the IDeckLinkStatus interface"); + } + + result = selected_device->QueryInterface(IID_IDeckLinkInput, (void**)&deckLink_input); + if (result != S_OK) { + throw std::runtime_error("Could not obtain the IDeckLinkInput interface - result"); + } + + input_callback = new DeckLinkInputCallback(this, detection_enabled, mutex, signal_condition); + if (!input_callback) { + throw std::runtime_error("Could not create input callback object"); + } + + result = deckLink_input->SetCallback(input_callback); + if (result != S_OK) { + throw std::runtime_error("Could not set input callback - result"); + } + + input_callback->set_decklink_input(deckLink_input); + + return S_OK; + } +}; + +} \ No newline at end of file