From c60c5645ed3d17b1006af77880dc503882fa3893 Mon Sep 17 00:00:00 2001 From: itten Date: Mon, 6 Jul 2026 01:27:19 +0300 Subject: [PATCH 1/5] 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 From 640271384e09ba182e09ea74c4d27e429840328b Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Mon, 6 Jul 2026 01:34:01 +0300 Subject: [PATCH 2/5] refactor: DeckLinkReceiver cleanup and encapsulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeckLinkReceiver: - Make InputCallback a private nested class — no longer exposed publicly - DeckLinkReceiver owns all shared state (mutex, CVs, frame buffer) - Replace get_input_callback() with clean wait_for_frame() API - Fix device list leak: enumerate_devices stores raw IDeckLink* in raw_devices; destructor releases all of them + selected_device's AddRef - Remove dead members: device_config, device_status, deckLink_notification - Remove dead SourceInfo::stride field; rename SourceInfo → VideoInfo - Remove dead video_source_info public member - Remove printf; use no logging in receiver (caller logs) - Remove commented-out notification code - Fix dead return false after throw in enumerate_devices - Fix dead null check after new InputCallback - Replace IDeckLinkVideoBuffer QueryInterface with simpler GetBytes() - Replace plain bool frame_ready/format_detected with consistent usage under mutex (no longer mixing atomic + CV pattern) - Call StopStreams/DisableVideoInput in destructor - Consistent snake_case naming throughout decklinkin main.cpp: - Rename NodeDeckLinkIn → DeckLinkInNode - Get device_index from config().value("device_index", 0u) - Remove unused includes: , , , - Use clean receiver.wait_for_frame() instead of reaching into callback - Use mxlGetCurrentIndex resync after sleep (consistent with other nodes) - Fix return node.execute() (was node.execute(); return 0) - Fix main() spacing Co-Authored-By: Claude Sonnet 4.6 --- nodes/decklinkin/main.cpp | 128 ++++----- shared/DeckLinkReceiver.hpp | 507 +++++++++++++----------------------- 2 files changed, 220 insertions(+), 415 deletions(-) diff --git a/nodes/decklinkin/main.cpp b/nodes/decklinkin/main.cpp index e04dbc1..956ceb3 100644 --- a/nodes/decklinkin/main.cpp +++ b/nodes/decklinkin/main.cpp @@ -1,123 +1,85 @@ -#include "NodeBase.hpp" -#include "DeckLinkReceiver.hpp" -#include "Signal.hpp" -#include "FlowDef.hpp" -#include +#include #include -#include -#include #include #include -#include -#include +#include "NodeBase.hpp" +#include "FlowDef.hpp" +#include "DeckLinkReceiver.hpp" -class NodeDeckLinkIn: public dmf::NodeBase { +class DeckLinkInNode : 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::DeckLinkReceiver receiver; - dmf::SourceInfo video_source_info{}; - if (!decklink_receiver.wait_for_format(5000)) { + log("Available DeckLink devices:"); + for (const auto& d : receiver.devices) + log(" %u) %s", d.index, d.name.c_str()); + + const uint32_t device_index = config().value("device_index", 0u); + receiver.start_capture(device_index); + log("Capturing from: %s", receiver.devices[device_index].name.c_str()); + + if (!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& vi = receiver.video_info; + if (vi.width == 0 || vi.fps_num == 0) { log("Invalid format detected"); return; } + log("Detected: %dx%d @ %d/%d fps", vi.width, vi.height, vi.fps_num, vi.fps_den); 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); + const int width = video_flow_info.value("width", vi.width); + const int height = video_flow_info.value("height", vi.height); + const int fps_num = video_flow_info.value("fps_num", vi.fps_num); + const int fps_den = video_flow_info.value("fps_den", vi.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{}; + mxlFlowWriter video_writer = nullptr; 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; } + if (vst != MXL_STATUS_OK) { + log("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)); + const mxlRational video_rate = {fps_num, fps_den}; + uint64_t video_index = mxlGetCurrentIndex(&video_rate); + log("start video_index=%llu", video_index); - auto* cb = decklink_receiver.get_input_callback(); - const uint32_t dst_row = video_stride; - std::vector local_frame(dst_row * height, 0); + std::vector frame_buf(static_cast(video_stride) * static_cast(height), 0); + + while (dmf::g_running.load(std::memory_order_relaxed)) { + if (!receiver.wait_for_frame(frame_buf.data(), video_stride, width, height)) break; - 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; + 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; + std::memcpy(video_buf, frame_buf.data(), frame_buf.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++; + video_index = mxlGetCurrentIndex(&video_rate); } } + + log("stopped at video_index=%llu", video_index); + mxlReleaseFlowWriter(instance(), video_writer); } }; -int main () -{ - NodeDeckLinkIn node; - node.execute(); - return 0; -} \ No newline at end of file +int main() { + DeckLinkInNode node; + return node.execute(); +} diff --git a/shared/DeckLinkReceiver.hpp b/shared/DeckLinkReceiver.hpp index 92b07b4..5afc2dc 100644 --- a/shared/DeckLinkReceiver.hpp +++ b/shared/DeckLinkReceiver.hpp @@ -1,374 +1,217 @@ #pragma once -#include -#include +#include #include +#include +#include +#include +#include #include +#include #include #include -#include -#include #include - -namespace dmf { class DeckLinkReceiver; } +#include "Signal.hpp" namespace dmf { -struct SourceInfo { - int width = 0; - int height = 0; - int fps_num = 0; - int fps_den = 0; - int stride = 0; -}; +class DeckLinkReceiver { +public: + struct VideoInfo { + int width = 0; + int height = 0; + int fps_num = 0; + int fps_den = 0; + }; -class DeckLinkInputCallback: public IDeckLinkInputCallback - { + struct DeviceInfo { + uint32_t index; + std::string name; + }; + + std::vector devices; + VideoInfo video_info{}; + + DeckLinkReceiver() { enumerate_devices(); } + + ~DeckLinkReceiver() { + if (decklink_input) { + decklink_input->StopStreams(); + decklink_input->DisableVideoInput(); + decklink_input->SetCallback(nullptr); + decklink_input->Release(); + } + delete input_callback; + for (auto* d : raw_devices) d->Release(); + if (selected_device) selected_device->Release(); + } + + void start_capture(uint32_t device_index) { + if (device_index >= raw_devices.size()) + throw std::runtime_error("Device index out of range"); + + selected_device = raw_devices[device_index]; + selected_device->AddRef(); + + HRESULT r = selected_device->QueryInterface(IID_IDeckLinkInput, (void**)&decklink_input); + if (r != S_OK) throw std::runtime_error("Could not obtain IDeckLinkInput"); + + input_callback = new InputCallback(*this); + r = decklink_input->SetCallback(input_callback); + if (r != S_OK) throw std::runtime_error("Could not set input callback"); + + r = decklink_input->EnableVideoInput(bmdModeNTSC, bmdFormat10BitYUV, + bmdVideoInputEnableFormatDetection); + if (r != S_OK) throw std::runtime_error("Could not enable video input"); + + r = decklink_input->StartStreams(); + if (r != S_OK) throw std::runtime_error("Could not start streams"); + } + + bool wait_for_format(uint64_t timeout_ms = 5000) { + std::unique_lock lk(mutex); + return format_cv.wait_for(lk, std::chrono::milliseconds(timeout_ms), + [this] { return format_detected; }); + } + + // Blocks until a fresh frame arrives or g_running goes false. + // Copies frame data into dst with MXL stride. Returns false on shutdown. + bool wait_for_frame(uint8_t* dst, uint32_t dst_stride, int width, int height) { + std::unique_lock lk(mutex); + frame_cv.wait(lk, [this] { + return frame_ready || !dmf::g_running.load(std::memory_order_relaxed); + }); + if (!dmf::g_running.load(std::memory_order_relaxed)) return false; + + const uint32_t src_stride = frame_row_bytes; + const int rows = std::min(frame_height, height); + const uint32_t copy_bytes = std::min(src_stride, dst_stride); + + std::memset(dst, 0, static_cast(dst_stride) * static_cast(height)); + const uint8_t* src = frame_buffer.data(); + uint8_t* d = dst; + for (int y = 0; y < rows; ++y, src += src_stride, d += dst_stride) + std::memcpy(d, src, copy_bytes); + + frame_ready = false; + return true; + } + +private: + class InputCallback : 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; } + explicit InputCallback(DeckLinkReceiver& owner) : owner(owner) {} HRESULT STDMETHODCALLTYPE VideoInputFormatChanged( - BMDVideoInputFormatChangedEvents notification_events, - IDeckLinkDisplayMode *new_display_mode, - BMDDetectedVideoInputFormatFlags detected_signal_flags - ) override + BMDVideoInputFormatChangedEvents events, + IDeckLinkDisplayMode* mode, + BMDDetectedVideoInputFormatFlags /*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 : "?"); + if (!(events & bmdVideoInputDisplayModeChanged)) return S_OK; - // 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); + std::lock_guard lk(owner.mutex); + owner.video_info.width = static_cast(mode->GetWidth()); + owner.video_info.height = static_cast(mode->GetHeight()); + BMDTimeValue dur = 0; BMDTimeScale ts = 0; + mode->GetFrameRate(&dur, &ts); + if (dur > 0 && ts > 0) { + owner.video_info.fps_num = static_cast(ts); + owner.video_info.fps_den = static_cast(dur); + const int g = std::gcd(owner.video_info.fps_num, owner.video_info.fps_den); + if (g > 1) { owner.video_info.fps_num /= g; owner.video_info.fps_den /= g; } } + owner.frame_ready = false; + owner.frame_buffer.clear(); + owner.format_detected = true; + } + owner.format_cv.notify_one(); + + HRESULT r = owner.decklink_input->StopStreams(); + if (r == S_OK) { + r = owner.decklink_input->EnableVideoInput( + mode->GetDisplayMode(), bmdFormat10BitYUV, + bmdVideoInputEnableFormatDetection); + if (r == S_OK) owner.decklink_input->StartStreams(); } return S_OK; } HRESULT STDMETHODCALLTYPE VideoInputFrameArrived( - IDeckLinkVideoInputFrame* video_frame, - IDeckLinkAudioInputPacket* audio_packet - ) override + 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(); + video_frame->GetBytes(&src); + if (!src) return S_OK; + + const uint32_t row_bytes = static_cast(video_frame->GetRowBytes()); + const int fw = static_cast(video_frame->GetWidth()); + const int fh = static_cast(video_frame->GetHeight()); + const size_t sz = static_cast(row_bytes) * static_cast(fh); + + { + std::lock_guard lk(owner.mutex); + if (owner.frame_buffer.size() < sz) owner.frame_buffer.resize(sz); + std::memcpy(owner.frame_buffer.data(), src, sz); + owner.frame_row_bytes = row_bytes; + owner.frame_width = fw; + owner.frame_height = fh; + owner.frame_ready = true; } - buf->EndAccess(bmdBufferAccessRead); - buf->Release(); + owner.frame_cv.notify_one(); 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 - } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, LPVOID*) override { return E_NOINTERFACE; } + ULONG STDMETHODCALLTYPE AddRef() override { return ++ref_count; } + ULONG STDMETHODCALLTYPE Release() override { return --ref_count; } 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; + DeckLinkReceiver& owner; + std::atomic ref_count{1}; + }; - std::atomic video_info_set{false}; - std::mutex video_info_mutex; + std::vector raw_devices; + IDeckLink* selected_device = nullptr; + IDeckLinkInput* decklink_input = nullptr; + InputCallback* input_callback = nullptr; - 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); - } - } -}; + std::mutex mutex; + std::condition_variable format_cv; + std::condition_variable frame_cv; + bool format_detected = false; -struct DeviceInfo { - uint32_t index; - IDeckLink* device; - std::string display_name; -}; + std::vector frame_buffer; + uint32_t frame_row_bytes = 0; + int frame_width = 0; + int frame_height = 0; + bool frame_ready = false; -class DeckLinkReceiver -{ - public: - SourceInfo video_source_info{}; - std::vector devices_list; + void enumerate_devices() { + IDeckLinkIterator* it = CreateDeckLinkIteratorInstance(); + if (!it) throw std::runtime_error("DeckLink drivers not installed"); - 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 : "?")}); + IDeckLink* device = nullptr; + uint32_t index = 0; + while (it->Next(&device) == S_OK) { + IDeckLinkInput* inp = nullptr; + if (device->QueryInterface(IID_IDeckLinkInput, (void**)&inp) == S_OK) { + inp->Release(); + const char* name = nullptr; + device->GetDisplayName(&name); + devices.push_back({index, name ? name : "?"}); + raw_devices.push_back(device); index++; + } else { + device->Release(); } - 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; } + it->Release(); + if (raw_devices.empty()) + throw std::runtime_error("No DeckLink input devices found"); + } }; -} \ No newline at end of file +} // namespace dmf From e335461aacbd31dca4f9c4f36075126926808c9c Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Mon, 6 Jul 2026 01:38:13 +0300 Subject: [PATCH 3/5] refactor: DeckLinkReceiver readability and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeckLinkReceiver: - Group private members with section comments (SDK objects / sync / frame state) - Change wait_for_format timeout param from uint64_t to int (matches std::chrono::milliseconds and all call sites) decklinkin main.cpp: - Wrap start_capture in try/catch — logs error and returns cleanly on device init failure (consistent with NDIInNode pattern) - Remove redundant zero-init on frame_buf (vector zero-inits anyway) Co-Authored-By: Claude Sonnet 4.6 --- nodes/decklinkin/main.cpp | 22 +++++++++++++--------- shared/DeckLinkReceiver.hpp | 9 ++++++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/nodes/decklinkin/main.cpp b/nodes/decklinkin/main.cpp index 956ceb3..de81b38 100644 --- a/nodes/decklinkin/main.cpp +++ b/nodes/decklinkin/main.cpp @@ -8,15 +8,19 @@ class DeckLinkInNode : public dmf::NodeBase { void run() override { - dmf::DeckLinkReceiver receiver; - - log("Available DeckLink devices:"); - for (const auto& d : receiver.devices) - log(" %u) %s", d.index, d.name.c_str()); - const uint32_t device_index = config().value("device_index", 0u); - receiver.start_capture(device_index); - log("Capturing from: %s", receiver.devices[device_index].name.c_str()); + + dmf::DeckLinkReceiver receiver; + try { + log("Available DeckLink devices:"); + for (const auto& d : receiver.devices) + log(" %u) %s", d.index, d.name.c_str()); + receiver.start_capture(device_index); + log("Capturing from: %s", receiver.devices[device_index].name.c_str()); + } catch (const std::runtime_error& e) { + log("DeckLink init error: %s", e.what()); + return; + } if (!receiver.wait_for_format(5000)) { log("Timeout waiting for format detection"); @@ -55,7 +59,7 @@ class DeckLinkInNode : public dmf::NodeBase { uint64_t video_index = mxlGetCurrentIndex(&video_rate); log("start video_index=%llu", video_index); - std::vector frame_buf(static_cast(video_stride) * static_cast(height), 0); + std::vector frame_buf(static_cast(video_stride) * static_cast(height)); while (dmf::g_running.load(std::memory_order_relaxed)) { if (!receiver.wait_for_frame(frame_buf.data(), video_stride, width, height)) break; diff --git a/shared/DeckLinkReceiver.hpp b/shared/DeckLinkReceiver.hpp index 5afc2dc..c463fe5 100644 --- a/shared/DeckLinkReceiver.hpp +++ b/shared/DeckLinkReceiver.hpp @@ -68,7 +68,7 @@ public: if (r != S_OK) throw std::runtime_error("Could not start streams"); } - bool wait_for_format(uint64_t timeout_ms = 5000) { + bool wait_for_format(int timeout_ms = 5000) { std::unique_lock lk(mutex); return format_cv.wait_for(lk, std::chrono::milliseconds(timeout_ms), [this] { return format_detected; }); @@ -173,16 +173,19 @@ private: std::atomic ref_count{1}; }; + // DeckLink SDK objects std::vector raw_devices; IDeckLink* selected_device = nullptr; IDeckLinkInput* decklink_input = nullptr; InputCallback* input_callback = nullptr; + // Synchronisation — one mutex guards all shared state below std::mutex mutex; - std::condition_variable format_cv; - std::condition_variable frame_cv; + std::condition_variable format_cv; // signalled when format is detected + std::condition_variable frame_cv; // signalled when a frame arrives bool format_detected = false; + // Frame state — written by InputCallback, read by wait_for_frame (both under mutex) std::vector frame_buffer; uint32_t frame_row_bytes = 0; int frame_width = 0; From 7847a8b70db1636e257e9f7caabb22063a82f5ca Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Mon, 6 Jul 2026 01:42:32 +0300 Subject: [PATCH 4/5] fix: use IDeckLinkVideoBuffer::GetBytes for frame access On Linux, GetBytes lives on IDeckLinkVideoBuffer (via QueryInterface), not directly on IDeckLinkVideoInputFrame. Also restores StartAccess/ EndAccess around the copy for correct buffer lifecycle management. Co-Authored-By: Claude Sonnet 4.6 --- shared/DeckLinkReceiver.hpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/shared/DeckLinkReceiver.hpp b/shared/DeckLinkReceiver.hpp index c463fe5..0f4c8f9 100644 --- a/shared/DeckLinkReceiver.hpp +++ b/shared/DeckLinkReceiver.hpp @@ -142,16 +142,20 @@ private: IDeckLinkAudioInputPacket* /*audio_packet*/) override { if (!video_frame) return S_OK; + + IDeckLinkVideoBuffer* buf = nullptr; + if (video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) != S_OK) + return S_OK; + buf->StartAccess(bmdBufferAccessRead); void* src = nullptr; - video_frame->GetBytes(&src); - if (!src) return S_OK; + buf->GetBytes(&src); - const uint32_t row_bytes = static_cast(video_frame->GetRowBytes()); - const int fw = static_cast(video_frame->GetWidth()); - const int fh = static_cast(video_frame->GetHeight()); - const size_t sz = static_cast(row_bytes) * static_cast(fh); + if (src) { + const uint32_t row_bytes = static_cast(video_frame->GetRowBytes()); + const int fw = static_cast(video_frame->GetWidth()); + const int fh = static_cast(video_frame->GetHeight()); + const size_t sz = static_cast(row_bytes) * static_cast(fh); - { std::lock_guard lk(owner.mutex); if (owner.frame_buffer.size() < sz) owner.frame_buffer.resize(sz); std::memcpy(owner.frame_buffer.data(), src, sz); @@ -160,7 +164,10 @@ private: owner.frame_height = fh; owner.frame_ready = true; } - owner.frame_cv.notify_one(); + + buf->EndAccess(bmdBufferAccessRead); + buf->Release(); + if (src) owner.frame_cv.notify_one(); return S_OK; } From 94da6f3283486595b28ff4d875a5d81d6fefb5fb Mon Sep 17 00:00:00 2001 From: JohannesItten Date: Mon, 6 Jul 2026 01:51:47 +0300 Subject: [PATCH 5/5] audio support --- ..._Media_Facility_Reference_Architecture.pdf | Bin 0 -> 3679247 bytes nodes/decklinkin/main.cpp | 140 ++++++++++++++---- shared/DeckLinkReceiver.hpp | 83 +++++++++-- 3 files changed, 182 insertions(+), 41 deletions(-) create mode 100644 EBU_White_Paper_The_Dynamic_Media_Facility_Reference_Architecture.pdf diff --git a/EBU_White_Paper_The_Dynamic_Media_Facility_Reference_Architecture.pdf b/EBU_White_Paper_The_Dynamic_Media_Facility_Reference_Architecture.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8b947b0290dcd6b7b043c1817d84c916678d82c4 GIT binary patch literal 3679247 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℞N0 zok}Poi~?Rt@b)8eg2A7wy`J*f_v8w~wbxpLMEh{h5`Xpne9#@auw(L=%{mZtl6F-UHj*^KSs)#oG>g})A?g|OLR2_u>$=qTDW|6A7umu5YrR*Shxv`G6^YqRZdEO7t zpz*Y~2C@_5>X4C%%&hBZdrfcAW!l!{)IzgUp7OgnU?@BIYd&`g!R8z1NHSJ!tf?o7K?kZSbwx?pfR?63DHhEd<5RC1*m z;#QklB86*_GKI!Jo^Fi)YCZ9~*t-2{0!;ESGXTdKU$^5U@!9R}im$oyxYfI(+lt2A zx4D$+IRie_ABbQ7pm$kr+O;DSBY~BDU2KsE0Aj)^WUM2H-RYg>+=lZF{jFGiY0j0W z6g1vjySx3oRrA&prNwMqU?DT>Y!N9qRikKF$_%btV__j=n0;r4Qt%|skg+Izj~`` z?KHpqgYw;e>b6+(Xqd(KS~?G^QLDh!6dzmzf;}HBbo)hkG(2o1XV*RGMa-d}V;^HRlBYWP`a8(SRtrxGtLgDoBo+qK= z1lh?%LosV-9*4pHpHSM@6?hcJe?vg9aJBu;0=8rlAd|kcq1q$prD-cqiB5$c+#qVp zB?umB^IGoBnc!y!XMaQDyDP1wKTxA;Tt|_GOGTk?#{tz=j~~O1aL{gWx8v5M(56gIJAI8#w;lKJ`a2bnJ@w(0ZWk6I;xroi@HX0|1 z-Z)SN;{^n!I9O7nu|QQB3kOX1K0kRpE<~0mXg?Ad7M@r+`K-9i(x25uvrV&d6qc9p z*c4@18vAemj(d0C)QOP%JPt| z)%w?RWirzl9FDo9Zuro0n%Tuo8zq6!ZM7aYEocY!hcx!b#4#1FVe*DehbcafrP_P9 zn13OoNsR!Tm(CS(Efa2OL55k-fvdz;u6Wk%$p4B{KZ_a?b`Q@1IaJv0lVlZk;xe~h zjORo8Kw{9_2}e6e1t+MilEJm(`z#ytOHdkJK}9c{srsnp5ia{4(^Cw`#RcI&-Y-!Q4X9H`0jkEdEsvxXv{%Fj!)bF