From d02223224f9e25eef2931c103d69de1319b269ad Mon Sep 17 00:00:00 2001 From: Johanness Date: Thu, 28 May 2026 22:23:00 +0300 Subject: [PATCH 01/14] feat: add DeckLink input and output nodes decklink-in: - IDeckLinkInputCallback::VideoInputFrameArrived captures V210 frames - Frame data stored with mutex, copied to MXL grain in process thread - Uses mxlSleepUntil for TAI-time-based grain pacing - Configurable: device_index (int), mode (1080i50/1080p50/etc) - Auto-detect input format via bmdVideoInputEnableFormatDetection - 1 output port: video_out (V210) decklink-out: - Reads V210 grains from MXL, schedules playback via DeckLink output - Creates IDeckLinkMutableVideoFrame, copies grain data, schedules - Uses ScheduledFrameCompleted callback for frame completion - Configurable: device_index (int), mode (1080i50/1080p50/etc) - 1 input port: video_in (V210) Both nodes: - Use DeckLinkAPIDispatch.cpp for CreateDeckLinkIteratorInstance - Access pixel data via IDeckLinkVideoBuffer (latest SDK API) - Graceful device open/close on writer/reader add/remove - Build conditionally via DMF_BUILD_DECKLINK + DECKLINK_SDK_DIR --- CMakeLists.txt | 8 + nodes/decklink-in/CMakeLists.txt | 21 ++ nodes/decklink-in/src/decklink_in_node.cpp | 240 +++++++++++++++++++ nodes/decklink-in/src/decklink_in_node.hpp | 82 +++++++ nodes/decklink-in/src/main.cpp | 6 + nodes/decklink-out/CMakeLists.txt | 21 ++ nodes/decklink-out/src/decklink_out_node.cpp | 238 ++++++++++++++++++ nodes/decklink-out/src/decklink_out_node.hpp | 78 ++++++ nodes/decklink-out/src/main.cpp | 6 + 9 files changed, 700 insertions(+) create mode 100644 nodes/decklink-in/CMakeLists.txt create mode 100644 nodes/decklink-in/src/decklink_in_node.cpp create mode 100644 nodes/decklink-in/src/decklink_in_node.hpp create mode 100644 nodes/decklink-in/src/main.cpp create mode 100644 nodes/decklink-out/CMakeLists.txt create mode 100644 nodes/decklink-out/src/decklink_out_node.cpp create mode 100644 nodes/decklink-out/src/decklink_out_node.hpp create mode 100644 nodes/decklink-out/src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fb1da31..c25c901 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,14 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") option(DMF_BUILD_TESTS "Build tests" ON) option(DMF_BUILD_MXL_TOOLS "Build MXL tools (testsrc, sink)" OFF) +option(DMF_BUILD_DECKLINK "Build DeckLink I/O nodes" ON) + +set(DECKLINK_SDK_DIR "" CACHE PATH "Path to Blackmagic DeckLink SDK root") + +if(DMF_BUILD_DECKLINK AND DECKLINK_SDK_DIR) + add_subdirectory(nodes/decklink-in) + add_subdirectory(nodes/decklink-out) +endif() find_package(fmt CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) diff --git a/nodes/decklink-in/CMakeLists.txt b/nodes/decklink-in/CMakeLists.txt new file mode 100644 index 0000000..9fe5ff9 --- /dev/null +++ b/nodes/decklink-in/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.24) + +project(dmf-node-decklink-in LANGUAGES CXX) + +set(DECKLINK_INCLUDE "${DECKLINK_SDK_DIR}/Linux/include") + +add_executable(dmf-node-decklink-in + src/main.cpp + src/decklink_in_node.cpp + "${DECKLINK_SDK_DIR}/Linux/include/DeckLinkAPIDispatch.cpp" +) + +target_include_directories(dmf-node-decklink-in PRIVATE + "${DECKLINK_INCLUDE}" +) + +target_link_libraries(dmf-node-decklink-in PRIVATE + dmf-node +) + +target_compile_options(dmf-node-decklink-in PRIVATE -Wno-unused-parameter) diff --git a/nodes/decklink-in/src/decklink_in_node.cpp b/nodes/decklink-in/src/decklink_in_node.cpp new file mode 100644 index 0000000..f21eeec --- /dev/null +++ b/nodes/decklink-in/src/decklink_in_node.cpp @@ -0,0 +1,240 @@ +#include "decklink_in_node.hpp" + +#include +#include +#include + +#include + +#include + +namespace dmf_node { + +DeckLinkInNode::~DeckLinkInNode() { + close_device(); +} + +void DeckLinkInNode::configure(const nlohmann::json& params) { + if (params.contains("device_index")) { + device_index_ = params["device_index"].get(); + } + if (params.contains("mode")) { + auto mode_str = params["mode"].get(); + if (mode_str == "1080i50") display_mode_ = bmdModeHD1080i50; + else if (mode_str == "1080p50") display_mode_ = bmdModeHD1080p50; + else if (mode_str == "1080p25") display_mode_ = bmdModeHD1080p25; + else if (mode_str == "1080i5994") display_mode_ = bmdModeHD1080i5994; + else if (mode_str == "1080p5994") display_mode_ = bmdModeHD1080p5994; + else if (mode_str == "1080p2997") display_mode_ = bmdModeHD1080p2997; + else if (mode_str == "720p50") display_mode_ = bmdModeHD720p50; + else if (mode_str == "720p5994") display_mode_ = bmdModeHD720p5994; + else { + spdlog::warn("DeckLink-in: unknown mode '{}', defaulting to 1080i50", mode_str); + } + } +} + +void DeckLinkInNode::on_add_writer(const std::string& port_id, mxlFlowWriter writer) { + if (port_id == "video_out") { + writer_ = writer; + + auto now = mxlGetTime(); + write_index_ = mxlTimestampToIndex(&grain_rate_, now); + + spdlog::info("DeckLink-in: writer added, grain_rate={}/{}", grain_rate_.numerator, grain_rate_.denominator); + + if (!open_device()) { + spdlog::error("DeckLink-in: failed to open device"); + return; + } + } +} + +void DeckLinkInNode::on_remove_writer(const std::string& port_id) { + if (port_id == "video_out") { + close_device(); + writer_.reset(); + spdlog::info("DeckLink-in: writer removed"); + } +} + +bool DeckLinkInNode::open_device() { + auto* iter = CreateDeckLinkIteratorInstance(); + if (!iter) { + spdlog::error("DeckLink-in: DeckLink drivers not found"); + return false; + } + + IDeckLink* device = nullptr; + for (int i = 0; i <= device_index_; ++i) { + if (iter->Next(&device) != S_OK) { + spdlog::error("DeckLink-in: device index {} not found", device_index_); + iter->Release(); + return false; + } + if (i < device_index_) { + device->Release(); + } + } + iter->Release(); + + const char* model_name = nullptr; + device->GetModelName(&model_name); + spdlog::info("DeckLink-in: opened device '{}'", model_name ? model_name : "unknown"); + + if (device->QueryInterface(IID_IDeckLinkInput, (void**)&input_) != S_OK) { + spdlog::error("DeckLink-in: device has no input interface"); + device->Release(); + return false; + } + + decklink_ = device; + + callback_ = std::make_unique(*this); + input_->SetCallback(callback_.get()); + + auto flags = bmdVideoInputEnableFormatDetection; + if (input_->EnableVideoInput(display_mode_, bmdFormat10BitYUV, flags) != S_OK) { + spdlog::error("DeckLink-in: failed to enable video input"); + return false; + } + + IDeckLinkDisplayMode* mode = nullptr; + if (input_->GetDisplayMode(display_mode_, &mode) == S_OK) { + frame_width_ = mode->GetWidth(); + frame_height_ = mode->GetHeight(); + BMDTimeValue duration = 0; + BMDTimeScale scale = 0; + mode->GetFrameRate(&duration, &scale); + mode->Release(); + spdlog::info("DeckLink-in: {}x{} @ {}/{} fps", frame_width_, frame_height_, duration, scale); + } + + if (input_->StartStreams() != S_OK) { + spdlog::error("DeckLink-in: failed to start streams"); + return false; + } + + capturing_ = true; + spdlog::info("DeckLink-in: capture started"); + return true; +} + +void DeckLinkInNode::close_device() { + if (input_) { + input_->StopStreams(); + input_->DisableVideoInput(); + input_->Release(); + input_ = nullptr; + } + if (decklink_) { + decklink_->Release(); + decklink_ = nullptr; + } + capturing_ = false; + callback_.reset(); + spdlog::info("DeckLink-in: device closed"); +} + +void DeckLinkInNode::process() { + if (!writer_ || !capturing_) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return; + } + + auto deadline = mxlIndexToTimestamp(&grain_rate_, write_index_ + 1); + mxlSleepUntil(deadline); + + void* src_data = nullptr; + long src_row_bytes = 0; + long src_width = 0; + long src_height = 0; + + { + std::lock_guard lock(frame_mutex_); + if (!frame_ready_ || !frame_data_) { + return; + } + src_data = frame_data_; + src_row_bytes = frame_row_bytes_; + src_width = frame_width_; + src_height = frame_height_; + frame_ready_ = false; + } + + mxlGrainInfo out_grain{}; + uint8_t* out_payload = nullptr; + auto status = mxlFlowWriterOpenGrain(*writer_, write_index_, &out_grain, &out_payload); + if (status != MXL_STATUS_OK) { + auto now = mxlGetTime(); + auto current = mxlTimestampToIndex(&grain_rate_, now); + write_index_ = current + 1; + return; + } + + auto dst_row_bytes = out_grain.grainSize / src_height; + auto copy_row_bytes = std::min(static_cast(dst_row_bytes), src_row_bytes); + + for (long y = 0; y < src_height && y < static_cast(out_grain.grainSize / dst_row_bytes); ++y) { + std::memcpy(out_payload + y * dst_row_bytes, + static_cast(src_data) + y * src_row_bytes, + copy_row_bytes); + } + + out_grain.validSlices = out_grain.totalSlices; + mxlFlowWriterCommitGrain(*writer_, &out_grain); + + write_index_++; + grains_written_++; + + if (grains_written_ == 1) { + spdlog::info("DeckLink-in: first grain written, index={}", write_index_ - 1); + } +} + +nlohmann::json DeckLinkInNode::status() const { + return { + {"type", "decklink-in"}, + {"grains_written", grains_written_}, + {"write_index", write_index_}, + {"capturing", capturing_.load()}, + {"has_writer", writer_.has_value()}, + }; +} + +HRESULT DeckLinkInNode::CaptureCallback::VideoInputFormatChanged( + BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode* newMode, BMDDetectedVideoInputFormatFlags) { + spdlog::info("DeckLink-in: input format changed"); + return S_OK; +} + +HRESULT DeckLinkInNode::CaptureCallback::VideoInputFrameArrived( + IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket*) { + if (!videoFrame || (videoFrame->GetFlags() & bmdFrameHasNoInputSource)) { + return S_OK; + } + + void* bytes = nullptr; + IDeckLinkVideoBuffer* buf = nullptr; + if (videoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) == S_OK && buf) { + buf->StartAccess(bmdBufferAccessRead); + buf->GetBytes(&bytes); + buf->EndAccess(bmdBufferAccessRead); + buf->Release(); + } + + if (!bytes) { + return S_OK; + } + + std::lock_guard lock(owner_.frame_mutex_); + owner_.frame_data_ = bytes; + owner_.frame_row_bytes_ = videoFrame->GetRowBytes(); + owner_.frame_width_ = videoFrame->GetWidth(); + owner_.frame_height_ = videoFrame->GetHeight(); + owner_.frame_ready_ = true; + + return S_OK; +} + +} // namespace dmf_node diff --git a/nodes/decklink-in/src/decklink_in_node.hpp b/nodes/decklink-in/src/decklink_in_node.hpp new file mode 100644 index 0000000..e55ec5b --- /dev/null +++ b/nodes/decklink-in/src/decklink_in_node.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace dmf_node { + +class DeckLinkInNode : public Node { +public: + DeckLinkInNode() = default; + ~DeckLinkInNode(); + + std::string type() const override { return "decklink-in"; } + + std::vector input_ports() const override { return {}; } + + std::vector output_ports() const override { + return {{"video_out", PortDirection::Output, MediaType::VideoV210}}; + } + + void configure(const nlohmann::json& params) override; + void on_add_writer(const std::string& port_id, mxlFlowWriter writer) override; + void on_add_reader(const std::string& port_id, mxlFlowReader reader) override {} + void on_remove_writer(const std::string& port_id) override; + void on_remove_reader(const std::string& port_id) override {} + + void process() override; + nlohmann::json status() const override; + +private: + bool open_device(); + void close_device(); + + std::optional writer_; + mxlRational grain_rate_{50, 1}; + uint64_t write_index_ = 0; + uint64_t grains_written_ = 0; + + int device_index_ = 0; + BMDDisplayMode display_mode_ = bmdModeHD1080i50; + + IDeckLink* decklink_ = nullptr; + IDeckLinkInput* input_ = nullptr; + + std::atomic capturing_{false}; + std::mutex frame_mutex_; + void* frame_data_ = nullptr; + long frame_row_bytes_ = 0; + long frame_width_ = 0; + long frame_height_ = 0; + bool frame_ready_ = false; + + class CaptureCallback : public IDeckLinkInputCallback { + public: + CaptureCallback(DeckLinkInNode& owner) : owner_(owner) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**) override { return E_NOINTERFACE; } + ULONG STDMETHODCALLTYPE AddRef() override { return 1; } + ULONG STDMETHODCALLTYPE Release() override { return 1; } + + HRESULT STDMETHODCALLTYPE VideoInputFormatChanged( + BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags) override; + + HRESULT STDMETHODCALLTYPE VideoInputFrameArrived( + IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket*) override; + + private: + DeckLinkInNode& owner_; + }; + + std::unique_ptr callback_; +}; + +} // namespace dmf_node diff --git a/nodes/decklink-in/src/main.cpp b/nodes/decklink-in/src/main.cpp new file mode 100644 index 0000000..59cde0a --- /dev/null +++ b/nodes/decklink-in/src/main.cpp @@ -0,0 +1,6 @@ +#include +#include "decklink_in_node.hpp" + +int main(int argc, char* argv[]) { + return dmf_node::NodeRunner::run(argc, argv); +} diff --git a/nodes/decklink-out/CMakeLists.txt b/nodes/decklink-out/CMakeLists.txt new file mode 100644 index 0000000..5962ec2 --- /dev/null +++ b/nodes/decklink-out/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.24) + +project(dmf-node-decklink-out LANGUAGES CXX) + +set(DECKLINK_INCLUDE "${DECKLINK_SDK_DIR}/Linux/include") + +add_executable(dmf-node-decklink-out + src/main.cpp + src/decklink_out_node.cpp + "${DECKLINK_SDK_DIR}/Linux/include/DeckLinkAPIDispatch.cpp" +) + +target_include_directories(dmf-node-decklink-out PRIVATE + "${DECKLINK_INCLUDE}" +) + +target_link_libraries(dmf-node-decklink-out PRIVATE + dmf-node +) + +target_compile_options(dmf-node-decklink-out PRIVATE -Wno-unused-parameter) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp new file mode 100644 index 0000000..f22e259 --- /dev/null +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -0,0 +1,238 @@ +#include "decklink_out_node.hpp" + +#include +#include +#include + +#include + +#include + +namespace dmf_node { + +DeckLinkOutNode::~DeckLinkOutNode() { + close_device(); +} + +void DeckLinkOutNode::configure(const nlohmann::json& params) { + if (params.contains("device_index")) { + device_index_ = params["device_index"].get(); + } + if (params.contains("mode")) { + auto mode_str = params["mode"].get(); + if (mode_str == "1080i50") display_mode_ = bmdModeHD1080i50; + else if (mode_str == "1080p50") display_mode_ = bmdModeHD1080p50; + else if (mode_str == "1080p25") display_mode_ = bmdModeHD1080p25; + else if (mode_str == "1080i5994") display_mode_ = bmdModeHD1080i5994; + else if (mode_str == "1080p5994") display_mode_ = bmdModeHD1080p5994; + else if (mode_str == "1080p2997") display_mode_ = bmdModeHD1080p2997; + else if (mode_str == "720p50") display_mode_ = bmdModeHD720p50; + else if (mode_str == "720p5994") display_mode_ = bmdModeHD720p5994; + else { + spdlog::warn("DeckLink-out: unknown mode '{}', defaulting to 1080i50", mode_str); + } + } +} + +void DeckLinkOutNode::on_add_reader(const std::string& port_id, mxlFlowReader reader) { + if (port_id == "video_in") { + reader_ = reader; + + mxlFlowConfigInfo config{}; + mxlFlowReaderGetConfigInfo(*reader_, &config); + grain_rate_ = config.common.grainRate; + + auto now = mxlGetTime(); + auto current_index = mxlTimestampToIndex(&grain_rate_, now); + read_index_ = current_index - 2; + + spdlog::info("DeckLink-out: reader added, grain_rate={}/{}", grain_rate_.numerator, grain_rate_.denominator); + + if (!open_device()) { + spdlog::error("DeckLink-out: failed to open device"); + return; + } + } +} + +void DeckLinkOutNode::on_remove_reader(const std::string& port_id) { + if (port_id == "video_in") { + close_device(); + reader_.reset(); + spdlog::info("DeckLink-out: reader removed"); + } +} + +bool DeckLinkOutNode::open_device() { + auto* iter = CreateDeckLinkIteratorInstance(); + if (!iter) { + spdlog::error("DeckLink-out: DeckLink drivers not found"); + return false; + } + + IDeckLink* device = nullptr; + for (int i = 0; i <= device_index_; ++i) { + if (iter->Next(&device) != S_OK) { + spdlog::error("DeckLink-out: device index {} not found", device_index_); + iter->Release(); + return false; + } + if (i < device_index_) { + device->Release(); + } + } + iter->Release(); + + const char* model_name = nullptr; + device->GetModelName(&model_name); + spdlog::info("DeckLink-out: opened device '{}'", model_name ? model_name : "unknown"); + + if (device->QueryInterface(IID_IDeckLinkOutput, (void**)&output_) != S_OK) { + spdlog::error("DeckLink-out: device has no output interface"); + device->Release(); + return false; + } + + decklink_ = device; + + callback_ = std::make_unique(*this); + output_->SetScheduledFrameCompletionCallback(callback_.get()); + + if (output_->EnableVideoOutput(display_mode_, bmdVideoOutputFlagDefault) != S_OK) { + spdlog::error("DeckLink-out: failed to enable video output"); + return false; + } + + IDeckLinkDisplayMode* mode = nullptr; + if (output_->GetDisplayMode(display_mode_, &mode) == S_OK) { + BMDTimeValue duration = 0; + BMDTimeScale scale = 0; + mode->GetFrameRate(&duration, &scale); + frame_duration_ = duration; + time_scale_ = scale; + mode->Release(); + } + + if (output_->StartScheduledPlayback(0, time_scale_, 1.0) != S_OK) { + spdlog::error("DeckLink-out: failed to start scheduled playback"); + return false; + } + + playing_ = true; + spdlog::info("DeckLink-out: playback started"); + return true; +} + +void DeckLinkOutNode::close_device() { + if (output_) { + output_->StopScheduledPlayback(0, nullptr, time_scale_); + output_->DisableVideoOutput(); + output_->Release(); + output_ = nullptr; + } + if (decklink_) { + decklink_->Release(); + decklink_ = nullptr; + } + playing_ = false; + callback_.reset(); + spdlog::info("DeckLink-out: device closed"); +} + +void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, long row_bytes) { + if (!output_) return; + + int32_t out_row_bytes = 0; + output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes); + + IDeckLinkMutableVideoFrame* frame = nullptr; + if (output_->CreateVideoFrame(width, height, out_row_bytes, bmdFormat10BitYUV, + bmdFrameFlagDefault, &frame) != S_OK) { + spdlog::warn("DeckLink-out: failed to create output frame"); + return; + } + + IDeckLinkVideoBuffer* buf = nullptr; + if (frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) == S_OK && buf) { + buf->StartAccess(bmdBufferAccessWrite); + void* dst = nullptr; + buf->GetBytes(&dst); + if (dst) { + auto copy_row_bytes = std::min(static_cast(out_row_bytes), row_bytes); + for (long y = 0; y < height; ++y) { + std::memcpy(static_cast(dst) + y * out_row_bytes, + static_cast(mxl_payload) + y * row_bytes, + copy_row_bytes); + } + } + buf->EndAccess(bmdBufferAccessWrite); + buf->Release(); + } + + auto stream_time = grains_read_ * frame_duration_; + output_->ScheduleVideoFrame(frame, stream_time, frame_duration_, time_scale_); + frame->Release(); +} + +void DeckLinkOutNode::process() { + if (!reader_ || !playing_) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return; + } + + auto deadline = mxlIndexToTimestamp(&grain_rate_, read_index_ + 1); + mxlSleepUntil(deadline); + + mxlGrainInfo grain_info{}; + uint8_t* payload = nullptr; + auto status = mxlFlowReaderGetGrain(*reader_, read_index_, 5000000ULL, &grain_info, &payload); + if (status != MXL_STATUS_OK) { + if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE || status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { + auto now = mxlGetTime(); + auto current_index = mxlTimestampToIndex(&grain_rate_, now); + read_index_ = current_index - 2; + if (grains_read_ == 0) { + spdlog::warn("DeckLink-out: realigned to index {}", read_index_); + } + } + return; + } + + mxlFlowConfigInfo config{}; + mxlFlowReaderGetConfigInfo(*reader_, &config); + auto grain_size = grain_info.grainSize; + auto height = config.discrete.grainCount > 0 ? 1080 : 1080; + auto row_bytes = grain_size / height; + + schedule_frame(payload, 1920, height, row_bytes); + + read_index_ = grain_info.index + 1; + grains_read_++; + + if (grains_read_ == 1) { + spdlog::info("DeckLink-out: first grain output, index={}", grain_info.index); + } +} + +nlohmann::json DeckLinkOutNode::status() const { + return { + {"type", "decklink-out"}, + {"grains_read", grains_read_}, + {"read_index", read_index_}, + {"playing", playing_.load()}, + {"has_reader", reader_.has_value()}, + }; +} + +HRESULT DeckLinkOutNode::OutputCallback::ScheduledFrameCompleted( + IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result) { + return S_OK; +} + +HRESULT DeckLinkOutNode::OutputCallback::ScheduledPlaybackHasStopped() { + owner_.playing_ = false; + spdlog::info("DeckLink-out: scheduled playback stopped"); + return S_OK; +} + +} // namespace dmf_node diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp new file mode 100644 index 0000000..027fce5 --- /dev/null +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace dmf_node { + +class DeckLinkOutNode : public Node { +public: + DeckLinkOutNode() = default; + ~DeckLinkOutNode(); + + std::string type() const override { return "decklink-out"; } + + std::vector input_ports() const override { + return {{"video_in", PortDirection::Input, MediaType::VideoV210}}; + } + + std::vector output_ports() const override { return {}; } + + void configure(const nlohmann::json& params) override; + void on_add_writer(const std::string& port_id, mxlFlowWriter writer) override {} + void on_add_reader(const std::string& port_id, mxlFlowReader reader) override; + void on_remove_writer(const std::string& port_id) override {} + void on_remove_reader(const std::string& port_id) override; + + void process() override; + nlohmann::json status() const override; + +private: + bool open_device(); + void close_device(); + void schedule_frame(void* mxl_payload, long width, long height, long row_bytes); + + std::optional reader_; + mxlRational grain_rate_{50, 1}; + uint64_t read_index_ = 0; + uint64_t grains_read_ = 0; + + int device_index_ = 0; + BMDDisplayMode display_mode_ = bmdModeHD1080i50; + + IDeckLink* decklink_ = nullptr; + IDeckLinkOutput* output_ = nullptr; + BMDTimeValue frame_duration_ = 1000; + BMDTimeScale time_scale_ = 50000; + + std::atomic playing_{false}; + + class OutputCallback : public IDeckLinkVideoOutputCallback { + public: + OutputCallback(DeckLinkOutNode& owner) : owner_(owner) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**) override { return E_NOINTERFACE; } + ULONG STDMETHODCALLTYPE AddRef() override { return 1; } + ULONG STDMETHODCALLTYPE Release() override { return 1; } + + HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted( + IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result) override; + HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped() override; + + private: + DeckLinkOutNode& owner_; + }; + + std::unique_ptr callback_; + IDeckLinkMutableVideoFrame* scheduled_frame_ = nullptr; +}; + +} // namespace dmf_node diff --git a/nodes/decklink-out/src/main.cpp b/nodes/decklink-out/src/main.cpp new file mode 100644 index 0000000..d061655 --- /dev/null +++ b/nodes/decklink-out/src/main.cpp @@ -0,0 +1,6 @@ +#include +#include "decklink_out_node.hpp" + +int main(int argc, char* argv[]) { + return dmf_node::NodeRunner::run(argc, argv); +} From 96bef6cd327c4398fafbf895f42b638420aa263f Mon Sep 17 00:00:00 2001 From: Johanness Date: Thu, 28 May 2026 22:51:45 +0300 Subject: [PATCH 02/14] fix: use relative symlink for extern/mxl (works on any checkout path) --- extern/mxl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/mxl b/extern/mxl index 90d5445..6ce7054 120000 --- a/extern/mxl +++ b/extern/mxl @@ -1 +1 @@ -/home/itten/DMF/mxl \ No newline at end of file +../../mxl \ No newline at end of file From 34f72c22f028d57d1d9db5a29facd018f1d4ebd8 Mon Sep 17 00:00:00 2001 From: Johanness Date: Thu, 28 May 2026 23:43:26 +0300 Subject: [PATCH 03/14] hardcoded decklink mode fix --- CMakeLists.txt | 1 + decklink_test.sh | 66 +++++++++++++++++++ extern/mxl | 2 +- .../dmf-engine/node_control_client.hpp | 1 + libs/dmf-engine/src/api_server.cpp | 23 ++++++- libs/dmf-engine/src/node_control_client.cpp | 56 ++++++++++++++++ .../include/dmf-node/control_server.hpp | 2 +- libs/dmf-node/src/control_server.cpp | 11 ++-- libs/dmf-node/src/node_runner.cpp | 25 ++++--- nodes/decklink-in/src/decklink_in_node.cpp | 51 ++++++++++---- nodes/decklink-in/src/decklink_in_node.hpp | 7 +- nodes/decklink-out/src/decklink_out_node.cpp | 12 +++- 12 files changed, 222 insertions(+), 35 deletions(-) create mode 100755 decklink_test.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index c25c901..12da608 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ find_package(nlohmann_json CONFIG REQUIRED) find_package(Libwebsockets CONFIG REQUIRED) find_package(Catch2 CONFIG QUIET) +set(BUILD_TESTS OFF CACHE BOOL "" FORCE) add_subdirectory(extern/mxl) add_subdirectory(libs/dmf-node) add_subdirectory(libs/dmf-engine) diff --git a/decklink_test.sh b/decklink_test.sh new file mode 100755 index 0000000..621a3e9 --- /dev/null +++ b/decklink_test.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +ENGINE_PORT=${ENGINE_PORT:-9000} +BASE_URL="http://127.0.0.1:${ENGINE_PORT}" +DEVICE_INDEX=${DEVICE_INDEX:-0} +MODE=${MODE:-1080i50} + +log() { echo "=== $1 ==="; } + +log "Adding decklink-in node (device=${DEVICE_INDEX}, mode=${MODE})" +curl -s -X POST "${BASE_URL}/api/graph/nodes" \ + -H "Content-Type: application/json" \ + -d "{\"type\":\"decklink-in\",\"id\":\"sdi_in\",\"config\":{\"device_index\":${DEVICE_INDEX},\"mode\":\"${MODE}\"}}" | python3 -m json.tool 2>/dev/null || echo "" + +log "Adding passthrough node" +curl -s -X POST "${BASE_URL}/api/graph/nodes" \ + -H "Content-Type: application/json" \ + -d '{"type":"passthrough","id":"pass1"}' | python3 -m json.tool 2>/dev/null || echo "" + +log "Adding decklink-out node (device=${DEVICE_INDEX}, mode=${MODE})" +curl -s -X POST "${BASE_URL}/api/graph/nodes" \ + -H "Content-Type: application/json" \ + -d "{\"type\":\"decklink-out\",\"id\":\"sdi_out\",\"config\":{\"device_index\":${DEVICE_INDEX},\"mode\":\"${MODE}\"}}" | python3 -m json.tool 2>/dev/null || echo "" + +log "Connecting sdi_in → pass1" +curl -s -X POST "${BASE_URL}/api/graph/edges" \ + -H "Content-Type: application/json" \ + -d '{"from_node":"sdi_in","from_port":"video_out","to_node":"pass1","to_port":"video_in"}' | python3 -m json.tool 2>/dev/null || echo "" + +log "Connecting pass1 → sdi_out" +curl -s -X POST "${BASE_URL}/api/graph/edges" \ + -H "Content-Type: application/json" \ + -d '{"from_node":"pass1","from_port":"video_out","to_node":"sdi_out","to_port":"video_in"}' | python3 -m json.tool 2>/dev/null || echo "" + +log "Starting graph" +curl -s -X POST "${BASE_URL}/api/graph/start" | python3 -m json.tool 2>/dev/null || echo "" + +sleep 3 + +log "Status: sdi_in" +curl -s -X POST "${BASE_URL}/api/graph/nodes/sdi_in/command" \ + -H "Content-Type: application/json" \ + -d '{"cmd":"status"}' | python3 -m json.tool 2>/dev/null || echo "" + +log "Status: pass1" +curl -s -X POST "${BASE_URL}/api/graph/nodes/pass1/command" \ + -H "Content-Type: application/json" \ + -d '{"cmd":"status"}' | python3 -m json.tool 2>/dev/null || echo "" + +log "Status: sdi_out" +curl -s -X POST "${BASE_URL}/api/graph/nodes/sdi_out/command" \ + -H "Content-Type: application/json" \ + -d '{"cmd":"status"}' | python3 -m json.tool 2>/dev/null || echo "" + +log "Graph state" +curl -s "${BASE_URL}/api/graph" | python3 -m json.tool 2>/dev/null || echo "" + +echo "" +echo "=== SDI pipeline running. Press Enter to stop. ===" +read + +log "Stopping graph" +curl -s -X POST "${BASE_URL}/api/graph/stop" | python3 -m json.tool 2>/dev/null || echo "" + +log "Done" diff --git a/extern/mxl b/extern/mxl index 6ce7054..a0c4b6b 120000 --- a/extern/mxl +++ b/extern/mxl @@ -1 +1 @@ -../../mxl \ No newline at end of file +../mxl \ No newline at end of file diff --git a/libs/dmf-engine/include/dmf-engine/node_control_client.hpp b/libs/dmf-engine/include/dmf-engine/node_control_client.hpp index 2b5d19f..8a19e2f 100644 --- a/libs/dmf-engine/include/dmf-engine/node_control_client.hpp +++ b/libs/dmf-engine/include/dmf-engine/node_control_client.hpp @@ -9,6 +9,7 @@ namespace dmf_engine { class NodeControlClient { public: bool send_command(uint16_t port, const std::string& json_cmd); + std::string send_command_with_response(uint16_t port, const std::string& json_cmd); void register_node(const std::string& node_id, uint16_t port); void unregister_node(const std::string& node_id); diff --git a/libs/dmf-engine/src/api_server.cpp b/libs/dmf-engine/src/api_server.cpp index e338668..7d44fcb 100644 --- a/libs/dmf-engine/src/api_server.cpp +++ b/libs/dmf-engine/src/api_server.cpp @@ -254,8 +254,29 @@ static void handle_request(const std::string& method, const std::string& path, return; } + int fps_num = 50, fps_den = 1; + int width = 1920, height = 1080; + + nlohmann::json status_cmd; + status_cmd["cmd"] = "status"; + auto status_resp = cc.send_command_with_response(port_num, status_cmd.dump()); + if (!status_resp.empty()) { + try { + auto sr = nlohmann::json::parse(status_resp); + if (sr.contains("data")) { + auto& d = sr["data"]; + if (d.contains("grain_rate")) { + fps_num = d["grain_rate"].value("numerator", fps_num); + fps_den = d["grain_rate"].value("denominator", fps_den); + } + width = d.value("width", width); + height = d.value("height", height); + } + } catch (...) {} + } + auto flow_id = fm.create_flow_id(); - auto flow_def = fm.create_v210_flow_def(flow_id, 1920, 1080, 50, 1); + auto flow_def = fm.create_v210_flow_def(flow_id, width, height, fps_num, fps_den); nlohmann::json cmd; cmd["cmd"] = "add_writer"; diff --git a/libs/dmf-engine/src/node_control_client.cpp b/libs/dmf-engine/src/node_control_client.cpp index 7ae62d4..299411b 100644 --- a/libs/dmf-engine/src/node_control_client.cpp +++ b/libs/dmf-engine/src/node_control_client.cpp @@ -85,4 +85,60 @@ bool NodeControlClient::send_command(uint16_t port, const std::string& json_cmd) return ok; } +std::string NodeControlClient::send_command_with_response(uint16_t port, const std::string& json_cmd) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + spdlog::error("NodeControlClient: socket() failed: {}", strerror(errno)); + return ""; + } + + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + struct sockaddr_in addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + if (connect(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + spdlog::error("NodeControlClient: connect to port {} failed: {}", port, strerror(errno)); + close(fd); + return ""; + } + + std::ostringstream req; + req << "POST /cmd HTTP/1.1\r\n" + << "Host: 127.0.0.1:" << port << "\r\n" + << "Content-Type: application/json\r\n" + << "Content-Length: " << json_cmd.size() << "\r\n" + << "Connection: close\r\n" + << "\r\n" + << json_cmd; + + auto request = req.str(); + auto sent = write(fd, request.data(), request.size()); + if (sent != static_cast(request.size())) { + spdlog::error("NodeControlClient: write failed on port {}", port); + close(fd); + return ""; + } + + std::string full_resp; + char resp_buf[4096]; + while (true) { + auto n = read(fd, resp_buf, sizeof(resp_buf)); + if (n <= 0) break; + full_resp.append(resp_buf, n); + } + close(fd); + + auto body_start = full_resp.find("\r\n\r\n"); + if (body_start == std::string::npos) return ""; + return full_resp.substr(body_start + 4); +} + } // namespace dmf_engine diff --git a/libs/dmf-node/include/dmf-node/control_server.hpp b/libs/dmf-node/include/dmf-node/control_server.hpp index 146423d..1cdb66d 100644 --- a/libs/dmf-node/include/dmf-node/control_server.hpp +++ b/libs/dmf-node/include/dmf-node/control_server.hpp @@ -11,7 +11,7 @@ struct lws; namespace dmf_node { -using CommandHandler = std::function; +using CommandHandler = std::function; using StatusCallback = std::function; class ControlServer { diff --git a/libs/dmf-node/src/control_server.cpp b/libs/dmf-node/src/control_server.cpp index acdd6b5..e9c06f8 100644 --- a/libs/dmf-node/src/control_server.cpp +++ b/libs/dmf-node/src/control_server.cpp @@ -29,17 +29,18 @@ struct ControlServer::Impl { struct lws_context* context = nullptr; }; -static void dispatch_command(ControlServerData* data, const nlohmann::json& msg) { +static nlohmann::json dispatch_command(ControlServerData* data, const nlohmann::json& msg) { if (!msg.contains("cmd")) { spdlog::warn("Control: message missing 'cmd' field"); - return; + return {{"error", "missing 'cmd' field"}}; } auto cmd = msg["cmd"].get(); auto it = data->commands.find(cmd); if (it != data->commands.end()) { - it->second(msg); + return it->second(msg); } else { spdlog::warn("Control: unknown command '{}'", cmd); + return {{"error", "unknown command: " + cmd}}; } } @@ -102,8 +103,8 @@ static int callback_all(struct lws* wsi, enum lws_callback_reasons reason, } try { auto msg = nlohmann::json::parse(ps->http_body); - dispatch_command(ps->data, msg); - return send_http_json(wsi, "200 OK", R"({"ok":true})"); + auto result = dispatch_command(ps->data, msg); + return send_http_json(wsi, "200 OK", result.dump()); } catch (const nlohmann::json::parse_error& e) { return send_http_json(wsi, "400 Bad Request", nlohmann::json({{"error", e.what()}}).dump()); diff --git a/libs/dmf-node/src/node_runner.cpp b/libs/dmf-node/src/node_runner.cpp index 1807b8c..51629f8 100644 --- a/libs/dmf-node/src/node_runner.cpp +++ b/libs/dmf-node/src/node_runner.cpp @@ -101,7 +101,7 @@ int NodeRunner::exec(std::unique_ptr node) { }; std::vector flow_resources; - control_server->register_command("add_writer", [&](const nlohmann::json& msg) { + control_server->register_command("add_writer", [&](const nlohmann::json& msg) -> nlohmann::json { auto flow_id = msg["flow_id"].get(); auto port_id = msg["port_id"].get(); auto flow_def = msg["flow_def"].dump(); @@ -112,14 +112,15 @@ int NodeRunner::exec(std::unique_ptr node) { auto status = mxlCreateFlowWriter(mxl_instance_, flow_def.c_str(), nullptr, &writer, &config_info, &created); if (status != MXL_STATUS_OK || !writer) { spdlog::error("Failed to create flow writer for flow {}: status={}", flow_id, static_cast(status)); - return; + return {{"ok", false}, {"error", "failed to create flow writer"}}; } spdlog::info("Created flow writer on port '{}' flow {} (created={})", port_id, flow_id, created); flow_resources.push_back({port_id, writer, nullptr}); node->on_add_writer(port_id, writer); + return {{"ok", true}}; }); - control_server->register_command("add_reader", [&](const nlohmann::json& msg) { + control_server->register_command("add_reader", [&](const nlohmann::json& msg) -> nlohmann::json { auto flow_id = msg["flow_id"].get(); auto port_id = msg["port_id"].get(); @@ -127,14 +128,15 @@ int NodeRunner::exec(std::unique_ptr node) { auto status = mxlCreateFlowReader(mxl_instance_, flow_id.c_str(), nullptr, &reader); if (status != MXL_STATUS_OK || !reader) { spdlog::error("Failed to create flow reader for flow {}: status={}", flow_id, static_cast(status)); - return; + return {{"ok", false}, {"error", "failed to create flow reader"}}; } spdlog::info("Created flow reader on port '{}' flow {}", port_id, flow_id); flow_resources.push_back({port_id, nullptr, reader}); node->on_add_reader(port_id, reader); + return {{"ok", true}}; }); - control_server->register_command("remove_writer", [&](const nlohmann::json& msg) { + control_server->register_command("remove_writer", [&](const nlohmann::json& msg) -> nlohmann::json { auto port_id = msg["port_id"].get(); node->on_remove_writer(port_id); for (auto it = flow_resources.begin(); it != flow_resources.end(); ++it) { @@ -145,9 +147,10 @@ int NodeRunner::exec(std::unique_ptr node) { } } spdlog::info("Removed writer on port '{}'", port_id); + return {{"ok", true}}; }); - control_server->register_command("remove_reader", [&](const nlohmann::json& msg) { + control_server->register_command("remove_reader", [&](const nlohmann::json& msg) -> nlohmann::json { auto port_id = msg["port_id"].get(); node->on_remove_reader(port_id); for (auto it = flow_resources.begin(); it != flow_resources.end(); ++it) { @@ -158,26 +161,30 @@ int NodeRunner::exec(std::unique_ptr node) { } } spdlog::info("Removed reader on port '{}'", port_id); + return {{"ok", true}}; }); - control_server->register_command("configure", [&](const nlohmann::json& msg) { + control_server->register_command("configure", [&](const nlohmann::json& msg) -> nlohmann::json { if (msg.contains("params")) { node->configure(msg["params"]); spdlog::info("Reconfigured node '{}'", node_id_); } + return {{"ok", true}}; }); - control_server->register_command("status", [&](const nlohmann::json& /*msg*/) { + control_server->register_command("status", [&](const nlohmann::json& /*msg*/) -> nlohmann::json { nlohmann::json resp; resp["event"] = "status"; resp["node_id"] = node_id_; resp["data"] = node->status(); control_server->send_event(resp); + return resp; }); - control_server->register_command("shutdown", [&](const nlohmann::json& /*msg*/) { + control_server->register_command("shutdown", [&](const nlohmann::json& /*msg*/) -> nlohmann::json { spdlog::info("Shutdown command received"); g_running = false; + return {{"ok", true}}; }); nlohmann::json ready_event; diff --git a/nodes/decklink-in/src/decklink_in_node.cpp b/nodes/decklink-in/src/decklink_in_node.cpp index f21eeec..7dc9e4f 100644 --- a/nodes/decklink-in/src/decklink_in_node.cpp +++ b/nodes/decklink-in/src/decklink_in_node.cpp @@ -32,6 +32,10 @@ void DeckLinkInNode::configure(const nlohmann::json& params) { spdlog::warn("DeckLink-in: unknown mode '{}', defaulting to 1080i50", mode_str); } } + + if (!open_device()) { + spdlog::error("DeckLink-in: failed to open device during configure"); + } } void DeckLinkInNode::on_add_writer(const std::string& port_id, mxlFlowWriter writer) { @@ -43,9 +47,13 @@ void DeckLinkInNode::on_add_writer(const std::string& port_id, mxlFlowWriter wri spdlog::info("DeckLink-in: writer added, grain_rate={}/{}", grain_rate_.numerator, grain_rate_.denominator); - if (!open_device()) { - spdlog::error("DeckLink-in: failed to open device"); - return; + if (input_ && !capturing_) { + if (input_->StartStreams() != S_OK) { + spdlog::error("DeckLink-in: failed to start streams"); + return; + } + capturing_ = true; + spdlog::info("DeckLink-in: capture started"); } } } @@ -107,22 +115,37 @@ bool DeckLinkInNode::open_device() { BMDTimeScale scale = 0; mode->GetFrameRate(&duration, &scale); mode->Release(); - spdlog::info("DeckLink-in: {}x{} @ {}/{} fps", frame_width_, frame_height_, duration, scale); + + is_interlaced_ = (display_mode_ == bmdModeHD1080i50 || + display_mode_ == bmdModeHD1080i5994); + + if (is_interlaced_) { + grain_rate_.numerator = static_cast(2 * scale); + grain_rate_.denominator = static_cast(duration); + } else { + grain_rate_.numerator = static_cast(scale); + grain_rate_.denominator = static_cast(duration); + } + + auto g = std::__gcd(grain_rate_.numerator, grain_rate_.denominator); + grain_rate_.numerator /= g; + grain_rate_.denominator /= g; + + spdlog::info("DeckLink-in: {}x{} @ {}/{} fps, interlaced={}", + frame_width_, frame_height_, + grain_rate_.numerator, grain_rate_.denominator, + is_interlaced_); } - if (input_->StartStreams() != S_OK) { - spdlog::error("DeckLink-in: failed to start streams"); - return false; - } - - capturing_ = true; - spdlog::info("DeckLink-in: capture started"); + spdlog::info("DeckLink-in: device opened, waiting for writer to start capture"); return true; } void DeckLinkInNode::close_device() { if (input_) { - input_->StopStreams(); + if (capturing_) { + input_->StopStreams(); + } input_->DisableVideoInput(); input_->Release(); input_ = nullptr; @@ -199,6 +222,10 @@ nlohmann::json DeckLinkInNode::status() const { {"write_index", write_index_}, {"capturing", capturing_.load()}, {"has_writer", writer_.has_value()}, + {"grain_rate", {{"numerator", grain_rate_.numerator}, {"denominator", grain_rate_.denominator}}}, + {"width", frame_width_}, + {"height", frame_height_}, + {"interlaced", is_interlaced_}, }; } diff --git a/nodes/decklink-in/src/decklink_in_node.hpp b/nodes/decklink-in/src/decklink_in_node.hpp index e55ec5b..3cc47a5 100644 --- a/nodes/decklink-in/src/decklink_in_node.hpp +++ b/nodes/decklink-in/src/decklink_in_node.hpp @@ -40,7 +40,7 @@ private: void close_device(); std::optional writer_; - mxlRational grain_rate_{50, 1}; + mxlRational grain_rate_{25, 1}; uint64_t write_index_ = 0; uint64_t grains_written_ = 0; @@ -51,11 +51,12 @@ private: IDeckLinkInput* input_ = nullptr; std::atomic capturing_{false}; + bool is_interlaced_ = false; std::mutex frame_mutex_; void* frame_data_ = nullptr; long frame_row_bytes_ = 0; - long frame_width_ = 0; - long frame_height_ = 0; + long frame_width_ = 1920; + long frame_height_ = 1080; bool frame_ready_ = false; class CaptureCallback : public IDeckLinkInputCallback { diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index f22e259..b54127a 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -201,10 +201,15 @@ void DeckLinkOutNode::process() { mxlFlowConfigInfo config{}; mxlFlowReaderGetConfigInfo(*reader_, &config); auto grain_size = grain_info.grainSize; - auto height = config.discrete.grainCount > 0 ? 1080 : 1080; - auto row_bytes = grain_size / height; - schedule_frame(payload, 1920, height, row_bytes); + long height = 1080; + if (config.discrete.sliceSizes[0] > 0) { + height = grain_size / config.discrete.sliceSizes[0]; + } + long row_bytes = (config.discrete.sliceSizes[0] > 0) ? static_cast(config.discrete.sliceSizes[0]) : (grain_size / height); + long width = (row_bytes * 3) / 8; + + schedule_frame(payload, width, height, row_bytes); read_index_ = grain_info.index + 1; grains_read_++; @@ -221,6 +226,7 @@ nlohmann::json DeckLinkOutNode::status() const { {"read_index", read_index_}, {"playing", playing_.load()}, {"has_reader", reader_.has_value()}, + {"grain_rate", {{"numerator", grain_rate_.numerator}, {"denominator", grain_rate_.denominator}}}, }; } From 8b5fb3dc363b57e63614fb3fd393666db8b5c3db Mon Sep 17 00:00:00 2001 From: Johanness Date: Thu, 28 May 2026 23:55:16 +0300 Subject: [PATCH 04/14] fix skipped write_index --- nodes/decklink-in/src/decklink_in_node.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/nodes/decklink-in/src/decklink_in_node.cpp b/nodes/decklink-in/src/decklink_in_node.cpp index 7dc9e4f..5bb0cba 100644 --- a/nodes/decklink-in/src/decklink_in_node.cpp +++ b/nodes/decklink-in/src/decklink_in_node.cpp @@ -175,16 +175,20 @@ void DeckLinkInNode::process() { { std::lock_guard lock(frame_mutex_); - if (!frame_ready_ || !frame_data_) { - return; + if (frame_data_) { + src_data = frame_data_; + src_row_bytes = frame_row_bytes_; + src_width = frame_width_; + src_height = frame_height_; } - src_data = frame_data_; - src_row_bytes = frame_row_bytes_; - src_width = frame_width_; - src_height = frame_height_; frame_ready_ = false; } + if (!src_data) { + write_index_++; + return; + } + mxlGrainInfo out_grain{}; uint8_t* out_payload = nullptr; auto status = mxlFlowWriterOpenGrain(*writer_, write_index_, &out_grain, &out_payload); From 2206fd71b38e970fd8e245b0e7a9f8999ae0934f Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 22:12:47 +0300 Subject: [PATCH 05/14] some fixes --- nodes/decklink-in/src/decklink_in_node.cpp | 35 ++++++++++++++++---- nodes/decklink-in/src/decklink_in_node.hpp | 1 + nodes/decklink-out/src/decklink_out_node.cpp | 34 +++++++++++++++++-- nodes/decklink-out/src/decklink_out_node.hpp | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/nodes/decklink-in/src/decklink_in_node.cpp b/nodes/decklink-in/src/decklink_in_node.cpp index 5bb0cba..bf5593e 100644 --- a/nodes/decklink-in/src/decklink_in_node.cpp +++ b/nodes/decklink-in/src/decklink_in_node.cpp @@ -4,6 +4,8 @@ #include #include +#include + #include #include @@ -32,6 +34,18 @@ void DeckLinkInNode::configure(const nlohmann::json& params) { spdlog::warn("DeckLink-in: unknown mode '{}', defaulting to 1080i50", mode_str); } } + if (params.contains("input_connection")) { + auto conn_str = params["input_connection"].get(); + if (conn_str == "sdi") input_connection_ = bmdVideoConnectionSDI; + else if (conn_str == "hdmi") input_connection_ = bmdVideoConnectionHDMI; + else if (conn_str == "optical_sdi") input_connection_ = bmdVideoConnectionOpticalSDI; + else if (conn_str == "component") input_connection_ = bmdVideoConnectionComponent; + else if (conn_str == "composite") input_connection_ = bmdVideoConnectionComposite; + else if (conn_str == "svideo") input_connection_ = bmdVideoConnectionSVideo; + else { + spdlog::warn("DeckLink-in: unknown input_connection '{}', defaulting to auto", conn_str); + } + } if (!open_device()) { spdlog::error("DeckLink-in: failed to open device during configure"); @@ -90,6 +104,18 @@ bool DeckLinkInNode::open_device() { device->GetModelName(&model_name); spdlog::info("DeckLink-in: opened device '{}'", model_name ? model_name : "unknown"); + if (input_connection_ != bmdVideoConnectionUnspecified) { + IDeckLinkConfiguration* config = nullptr; + if (device->QueryInterface(IID_IDeckLinkConfiguration, (void**)&config) == S_OK && config) { + if (config->SetInt(bmdDeckLinkConfigVideoInputConnection, input_connection_) != S_OK) { + spdlog::warn("DeckLink-in: failed to set input connection"); + } else { + spdlog::info("DeckLink-in: input connection configured"); + } + config->Release(); + } + } + if (device->QueryInterface(IID_IDeckLinkInput, (void**)&input_) != S_OK) { spdlog::error("DeckLink-in: device has no input interface"); device->Release(); @@ -119,13 +145,8 @@ bool DeckLinkInNode::open_device() { is_interlaced_ = (display_mode_ == bmdModeHD1080i50 || display_mode_ == bmdModeHD1080i5994); - if (is_interlaced_) { - grain_rate_.numerator = static_cast(2 * scale); - grain_rate_.denominator = static_cast(duration); - } else { - grain_rate_.numerator = static_cast(scale); - grain_rate_.denominator = static_cast(duration); - } + grain_rate_.numerator = static_cast(scale); + grain_rate_.denominator = static_cast(duration); auto g = std::__gcd(grain_rate_.numerator, grain_rate_.denominator); grain_rate_.numerator /= g; diff --git a/nodes/decklink-in/src/decklink_in_node.hpp b/nodes/decklink-in/src/decklink_in_node.hpp index 3cc47a5..60b1cf5 100644 --- a/nodes/decklink-in/src/decklink_in_node.hpp +++ b/nodes/decklink-in/src/decklink_in_node.hpp @@ -46,6 +46,7 @@ private: int device_index_ = 0; BMDDisplayMode display_mode_ = bmdModeHD1080i50; + BMDVideoConnection input_connection_ = bmdVideoConnectionUnspecified; IDeckLink* decklink_ = nullptr; IDeckLinkInput* input_ = nullptr; diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index b54127a..a437666 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -4,6 +4,8 @@ #include #include +#include + #include #include @@ -32,6 +34,18 @@ void DeckLinkOutNode::configure(const nlohmann::json& params) { spdlog::warn("DeckLink-out: unknown mode '{}', defaulting to 1080i50", mode_str); } } + if (params.contains("output_connection")) { + auto conn_str = params["output_connection"].get(); + if (conn_str == "sdi") output_connection_ = bmdVideoConnectionSDI; + else if (conn_str == "hdmi") output_connection_ = bmdVideoConnectionHDMI; + else if (conn_str == "optical_sdi") output_connection_ = bmdVideoConnectionOpticalSDI; + else if (conn_str == "component") output_connection_ = bmdVideoConnectionComponent; + else if (conn_str == "composite") output_connection_ = bmdVideoConnectionComposite; + else if (conn_str == "svideo") output_connection_ = bmdVideoConnectionSVideo; + else { + spdlog::warn("DeckLink-out: unknown output_connection '{}', defaulting to auto", conn_str); + } + } } void DeckLinkOutNode::on_add_reader(const std::string& port_id, mxlFlowReader reader) { @@ -95,6 +109,20 @@ bool DeckLinkOutNode::open_device() { decklink_ = device; + if (output_connection_ != bmdVideoConnectionUnspecified) { + IDeckLinkConfiguration* config = nullptr; + if (decklink_->QueryInterface(IID_IDeckLinkConfiguration, (void**)&config) == S_OK && config) { + if (config->SetInt(bmdDeckLinkConfigVideoOutputConnection, output_connection_) != S_OK) { + spdlog::warn("DeckLink-out: failed to set output connection"); + } else { + spdlog::info("DeckLink-out: set output connection configured"); + } + config->Release(); + } else { + spdlog::warn("DeckLink-out: IDeckLinkConfiguration not available"); + } + } + callback_ = std::make_unique(*this); output_->SetScheduledFrameCompletionCallback(callback_.get()); @@ -143,12 +171,14 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, if (!output_) return; int32_t out_row_bytes = 0; - output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes); + if (output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes) != S_OK || out_row_bytes <= 0) { + out_row_bytes = ((width + 5) / 6) * 16; + } IDeckLinkMutableVideoFrame* frame = nullptr; if (output_->CreateVideoFrame(width, height, out_row_bytes, bmdFormat10BitYUV, bmdFrameFlagDefault, &frame) != S_OK) { - spdlog::warn("DeckLink-out: failed to create output frame"); + spdlog::warn("DeckLink-out: failed to create output frame {}x{}", width, height); return; } diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index 027fce5..5807088 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -47,6 +47,7 @@ private: int device_index_ = 0; BMDDisplayMode display_mode_ = bmdModeHD1080i50; + BMDVideoConnection output_connection_ = bmdVideoConnectionUnspecified; IDeckLink* decklink_ = nullptr; IDeckLinkOutput* output_ = nullptr; From 003279669e86ecb58670decc8a49b1a7b74d635d Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:07:15 +0300 Subject: [PATCH 06/14] fix output res&frame rate --- nodes/decklink-out/src/decklink_out_node.cpp | 53 +++++++++++++------- nodes/decklink-out/src/decklink_out_node.hpp | 6 ++- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index a437666..4278194 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -54,13 +54,13 @@ void DeckLinkOutNode::on_add_reader(const std::string& port_id, mxlFlowReader re mxlFlowConfigInfo config{}; mxlFlowReaderGetConfigInfo(*reader_, &config); - grain_rate_ = config.common.grainRate; + flow_rate_ = config.common.grainRate; auto now = mxlGetTime(); - auto current_index = mxlTimestampToIndex(&grain_rate_, now); + auto current_index = mxlTimestampToIndex(&flow_rate_, now); read_index_ = current_index - 2; - spdlog::info("DeckLink-out: reader added, grain_rate={}/{}", grain_rate_.numerator, grain_rate_.denominator); + spdlog::info("DeckLink-out: reader added, flow_rate={}/{}", flow_rate_.numerator, flow_rate_.denominator); if (!open_device()) { spdlog::error("DeckLink-out: failed to open device"); @@ -133,12 +133,28 @@ bool DeckLinkOutNode::open_device() { IDeckLinkDisplayMode* mode = nullptr; if (output_->GetDisplayMode(display_mode_, &mode) == S_OK) { + out_width_ = mode->GetWidth(); + out_height_ = mode->GetHeight(); BMDTimeValue duration = 0; BMDTimeScale scale = 0; mode->GetFrameRate(&duration, &scale); frame_duration_ = duration; time_scale_ = scale; mode->Release(); + + output_rate_.numerator = static_cast(scale); + output_rate_.denominator = static_cast(duration); + auto g = std::__gcd(output_rate_.numerator, output_rate_.denominator); + output_rate_.numerator /= g; + output_rate_.denominator /= g; + + is_interlaced_ = (display_mode_ == bmdModeHD1080i50 || + display_mode_ == bmdModeHD1080i5994); + + spdlog::info("DeckLink-out: {}x{} @ {}/{} fps, interlaced={}", + out_width_, out_height_, + output_rate_.numerator, output_rate_.denominator, + is_interlaced_); } if (output_->StartScheduledPlayback(0, time_scale_, 1.0) != S_OK) { @@ -210,19 +226,22 @@ void DeckLinkOutNode::process() { return; } - auto deadline = mxlIndexToTimestamp(&grain_rate_, read_index_ + 1); + auto deadline = mxlIndexToTimestamp(&output_rate_, grains_read_ + 1); mxlSleepUntil(deadline); + auto out_timestamp = mxlIndexToTimestamp(&output_rate_, grains_read_); + auto source_index = mxlTimestampToIndex(&flow_rate_, out_timestamp); + mxlGrainInfo grain_info{}; uint8_t* payload = nullptr; - auto status = mxlFlowReaderGetGrain(*reader_, read_index_, 5000000ULL, &grain_info, &payload); + auto status = mxlFlowReaderGetGrain(*reader_, source_index, 5000000ULL, &grain_info, &payload); if (status != MXL_STATUS_OK) { if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE || status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { auto now = mxlGetTime(); - auto current_index = mxlTimestampToIndex(&grain_rate_, now); + auto current_index = mxlTimestampToIndex(&flow_rate_, now); read_index_ = current_index - 2; if (grains_read_ == 0) { - spdlog::warn("DeckLink-out: realigned to index {}", read_index_); + spdlog::warn("DeckLink-out: realigned to source index {}", read_index_); } } return; @@ -232,20 +251,17 @@ void DeckLinkOutNode::process() { mxlFlowReaderGetConfigInfo(*reader_, &config); auto grain_size = grain_info.grainSize; - long height = 1080; - if (config.discrete.sliceSizes[0] > 0) { - height = grain_size / config.discrete.sliceSizes[0]; - } - long row_bytes = (config.discrete.sliceSizes[0] > 0) ? static_cast(config.discrete.sliceSizes[0]) : (grain_size / height); - long width = (row_bytes * 3) / 8; + long src_row_bytes = (config.discrete.sliceSizes[0] > 0) ? static_cast(config.discrete.sliceSizes[0]) : (grain_size / out_height_); + long src_height = (src_row_bytes > 0) ? (grain_size / src_row_bytes) : out_height_; + long src_width = (src_row_bytes * 3) / 8; - schedule_frame(payload, width, height, row_bytes); + schedule_frame(payload, src_width, src_height, src_row_bytes); - read_index_ = grain_info.index + 1; + read_index_ = source_index + 1; grains_read_++; if (grains_read_ == 1) { - spdlog::info("DeckLink-out: first grain output, index={}", grain_info.index); + spdlog::info("DeckLink-out: first grain output, source_index={}", source_index); } } @@ -256,7 +272,10 @@ nlohmann::json DeckLinkOutNode::status() const { {"read_index", read_index_}, {"playing", playing_.load()}, {"has_reader", reader_.has_value()}, - {"grain_rate", {{"numerator", grain_rate_.numerator}, {"denominator", grain_rate_.denominator}}}, + {"flow_rate", {{"numerator", flow_rate_.numerator}, {"denominator", flow_rate_.denominator}}}, + {"output_rate", {{"numerator", output_rate_.numerator}, {"denominator", output_rate_.denominator}}}, + {"width", out_width_}, + {"height", out_height_}, }; } diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index 5807088..ea0ec26 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -41,7 +41,8 @@ private: void schedule_frame(void* mxl_payload, long width, long height, long row_bytes); std::optional reader_; - mxlRational grain_rate_{50, 1}; + mxlRational flow_rate_{50, 1}; + mxlRational output_rate_{50, 1}; uint64_t read_index_ = 0; uint64_t grains_read_ = 0; @@ -55,6 +56,9 @@ private: BMDTimeScale time_scale_ = 50000; std::atomic playing_{false}; + bool is_interlaced_ = false; + long out_width_ = 1920; + long out_height_ = 1080; class OutputCallback : public IDeckLinkVideoOutputCallback { public: From c2e823beda580ba3fa74ace4ed8b47149908cc15 Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:10:35 +0300 Subject: [PATCH 07/14] fix realignment --- nodes/decklink-out/src/decklink_out_node.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index 4278194..e51a641 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -239,12 +239,16 @@ void DeckLinkOutNode::process() { if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE || status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { auto now = mxlGetTime(); auto current_index = mxlTimestampToIndex(&flow_rate_, now); - read_index_ = current_index - 2; - if (grains_read_ == 0) { - spdlog::warn("DeckLink-out: realigned to source index {}", read_index_); + spdlog::warn("DeckLink-out: realigned from {} to {}", source_index, current_index - 2); + source_index = current_index - 2; + + status = mxlFlowReaderGetGrain(*reader_, source_index, 5000000ULL, &grain_info, &payload); + if (status != MXL_STATUS_OK) { + return; } + } else { + return; } - return; } mxlFlowConfigInfo config{}; From b5d9c7cd3ff392151bccc4f387e2fdb26c6254cf Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:13:24 +0300 Subject: [PATCH 08/14] fix decklink-out: init grains_read from current time to avoid epoch realignment --- nodes/decklink-out/src/decklink_out_node.cpp | 7 +++++++ nodes/decklink-out/src/decklink_out_node.hpp | 1 + 2 files changed, 8 insertions(+) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index e51a641..d836a9b 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -226,6 +226,13 @@ void DeckLinkOutNode::process() { return; } + if (first_frame_) { + auto now = mxlGetTime(); + grains_read_ = mxlTimestampToIndex(&output_rate_, now); + first_frame_ = false; + spdlog::info("DeckLink-out: starting at output index {}", grains_read_); + } + auto deadline = mxlIndexToTimestamp(&output_rate_, grains_read_ + 1); mxlSleepUntil(deadline); diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index ea0ec26..1dbcf8a 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -45,6 +45,7 @@ private: mxlRational output_rate_{50, 1}; uint64_t read_index_ = 0; uint64_t grains_read_ = 0; + bool first_frame_ = true; int device_index_ = 0; BMDDisplayMode display_mode_ = bmdModeHD1080i50; From b4f766fb9d8f479ff8aadb4086a94a6b9e7c9758 Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:15:00 +0300 Subject: [PATCH 09/14] add HRESULT logging to CreateVideoFrame for debug --- nodes/decklink-out/src/decklink_out_node.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index d836a9b..d358ad9 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -187,14 +187,19 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, if (!output_) return; int32_t out_row_bytes = 0; - if (output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes) != S_OK || out_row_bytes <= 0) { + HRESULT row_hr = output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes); + if (row_hr != S_OK || out_row_bytes <= 0) { + spdlog::warn("DeckLink-out: RowBytesForPixelFormat failed hr=0x{:08x}, using fallback", static_cast(row_hr)); out_row_bytes = ((width + 5) / 6) * 16; } + spdlog::debug("DeckLink-out: creating frame {}x{} row_bytes={}", width, height, out_row_bytes); IDeckLinkMutableVideoFrame* frame = nullptr; - if (output_->CreateVideoFrame(width, height, out_row_bytes, bmdFormat10BitYUV, - bmdFrameFlagDefault, &frame) != S_OK) { - spdlog::warn("DeckLink-out: failed to create output frame {}x{}", width, height); + HRESULT hr = output_->CreateVideoFrame(width, height, out_row_bytes, bmdFormat10BitYUV, + bmdFrameFlagDefault, &frame); + if (hr != S_OK || !frame) { + spdlog::warn("DeckLink-out: failed to create output frame {}x{} row_bytes={} hr=0x{:08x}", + width, height, out_row_bytes, static_cast(hr)); return; } From a3cd45cc4930e3a2022a7cece053828240be98dc Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:18:02 +0300 Subject: [PATCH 10/14] decklink-out: add mode support check, limit CreateVideoFrame error spam --- nodes/decklink-out/src/decklink_out_node.cpp | 27 +++++++++++++++++--- nodes/decklink-out/src/decklink_out_node.hpp | 1 + 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index d358ad9..1374256 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -126,6 +126,20 @@ bool DeckLinkOutNode::open_device() { callback_ = std::make_unique(*this); output_->SetScheduledFrameCompletionCallback(callback_.get()); + { + BMDDisplayMode actual_mode = bmdModeUnknown; + bool supported = false; + auto hr = output_->DoesSupportVideoMode(bmdVideoConnectionUnspecified, display_mode_, bmdFormat10BitYUV, + bmdNoVideoOutputConversion, bmdSupportedVideoModeDefault, &actual_mode, &supported); + if (hr == S_OK) { + spdlog::info("DeckLink-out: mode 0x{:x} V210 supported={} actual=0x{:x}", + static_cast(display_mode_), supported, static_cast(actual_mode)); + if (!supported) { + spdlog::error("DeckLink-out: display mode not supported for V210 output"); + } + } + } + if (output_->EnableVideoOutput(display_mode_, bmdVideoOutputFlagDefault) != S_OK) { spdlog::error("DeckLink-out: failed to enable video output"); return false; @@ -189,17 +203,22 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, int32_t out_row_bytes = 0; HRESULT row_hr = output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes); if (row_hr != S_OK || out_row_bytes <= 0) { - spdlog::warn("DeckLink-out: RowBytesForPixelFormat failed hr=0x{:08x}, using fallback", static_cast(row_hr)); + spdlog::warn("DeckLink-out: RowBytesForPixelFormat failed hr=0x{:08x}", static_cast(row_hr)); out_row_bytes = ((width + 5) / 6) * 16; } - spdlog::debug("DeckLink-out: creating frame {}x{} row_bytes={}", width, height, out_row_bytes); IDeckLinkMutableVideoFrame* frame = nullptr; HRESULT hr = output_->CreateVideoFrame(width, height, out_row_bytes, bmdFormat10BitYUV, bmdFrameFlagDefault, &frame); if (hr != S_OK || !frame) { - spdlog::warn("DeckLink-out: failed to create output frame {}x{} row_bytes={} hr=0x{:08x}", - width, height, out_row_bytes, static_cast(hr)); + if (create_fail_count_ < 5) { + spdlog::error("DeckLink-out: CreateVideoFrame {}x{} row_bytes={} fmt=V210 hr=0x{:08x}", + width, height, out_row_bytes, static_cast(hr)); + create_fail_count_++; + } + if (create_fail_count_ == 5) { + spdlog::error("DeckLink-out: suppressing further CreateVideoFrame errors"); + } return; } diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index 1dbcf8a..4a369db 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -57,6 +57,7 @@ private: BMDTimeScale time_scale_ = 50000; std::atomic playing_{false}; + int create_fail_count_ = 0; bool is_interlaced_ = false; long out_width_ = 1920; long out_height_ = 1080; From 26febc41d6dded0baa3bb98bae00410c3eda6f4a Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:21:13 +0300 Subject: [PATCH 11/14] decklink-out: preroll frames before StartScheduledPlayback, detect output format --- nodes/decklink-out/src/decklink_out_node.cpp | 66 +++++++++++++------- nodes/decklink-out/src/decklink_out_node.hpp | 2 + 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index 1374256..4ff620a 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -126,20 +126,6 @@ bool DeckLinkOutNode::open_device() { callback_ = std::make_unique(*this); output_->SetScheduledFrameCompletionCallback(callback_.get()); - { - BMDDisplayMode actual_mode = bmdModeUnknown; - bool supported = false; - auto hr = output_->DoesSupportVideoMode(bmdVideoConnectionUnspecified, display_mode_, bmdFormat10BitYUV, - bmdNoVideoOutputConversion, bmdSupportedVideoModeDefault, &actual_mode, &supported); - if (hr == S_OK) { - spdlog::info("DeckLink-out: mode 0x{:x} V210 supported={} actual=0x{:x}", - static_cast(display_mode_), supported, static_cast(actual_mode)); - if (!supported) { - spdlog::error("DeckLink-out: display mode not supported for V210 output"); - } - } - } - if (output_->EnableVideoOutput(display_mode_, bmdVideoOutputFlagDefault) != S_OK) { spdlog::error("DeckLink-out: failed to enable video output"); return false; @@ -171,6 +157,39 @@ bool DeckLinkOutNode::open_device() { is_interlaced_); } + preroll_frames_ = 3; + for (int i = 0; i < preroll_frames_; ++i) { + IDeckLinkMutableVideoFrame* frame = nullptr; + int32_t out_row_bytes = out_width_ * 16 / 6; + if (output_->CreateVideoFrame(out_width_, out_height_, out_row_bytes, + bmdFormat10BitYUV, bmdFrameFlagDefault, &frame) != S_OK || !frame) { + out_row_bytes = out_width_ * 2; + if (output_->CreateVideoFrame(out_width_, out_height_, out_row_bytes, + bmdFormat8BitYUV, bmdFrameFlagDefault, &frame) != S_OK || !frame) { + spdlog::error("DeckLink-out: failed to create preroll frame"); + return false; + } + spdlog::info("DeckLink-out: using 8BitYUV output format"); + output_format_ = bmdFormat8BitYUV; + } else { + output_format_ = bmdFormat10BitYUV; + } + IDeckLinkVideoBuffer* buf = nullptr; + if (frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) == S_OK && buf) { + buf->StartAccess(bmdBufferAccessWrite); + void* dst = nullptr; + buf->GetBytes(&dst); + if (dst) { + std::memset(dst, 0, out_row_bytes * out_height_); + } + buf->EndAccess(bmdBufferAccessWrite); + buf->Release(); + } + auto stream_time = i * frame_duration_; + output_->ScheduleVideoFrame(frame, stream_time, frame_duration_, time_scale_); + frame->Release(); + } + if (output_->StartScheduledPlayback(0, time_scale_, 1.0) != S_OK) { spdlog::error("DeckLink-out: failed to start scheduled playback"); return false; @@ -201,24 +220,23 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, if (!output_) return; int32_t out_row_bytes = 0; - HRESULT row_hr = output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes); - if (row_hr != S_OK || out_row_bytes <= 0) { - spdlog::warn("DeckLink-out: RowBytesForPixelFormat failed hr=0x{:08x}", static_cast(row_hr)); - out_row_bytes = ((width + 5) / 6) * 16; + if (output_format_ == bmdFormat10BitYUV) { + if (output_->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &out_row_bytes) != S_OK || out_row_bytes <= 0) { + out_row_bytes = ((width + 5) / 6) * 16; + } + } else { + out_row_bytes = width * 2; } IDeckLinkMutableVideoFrame* frame = nullptr; - HRESULT hr = output_->CreateVideoFrame(width, height, out_row_bytes, bmdFormat10BitYUV, + HRESULT hr = output_->CreateVideoFrame(width, height, out_row_bytes, output_format_, bmdFrameFlagDefault, &frame); if (hr != S_OK || !frame) { if (create_fail_count_ < 5) { - spdlog::error("DeckLink-out: CreateVideoFrame {}x{} row_bytes={} fmt=V210 hr=0x{:08x}", - width, height, out_row_bytes, static_cast(hr)); + spdlog::error("DeckLink-out: CreateVideoFrame {}x{} row_bytes={} fmt=0x{:x} hr=0x{:08x}", + width, height, out_row_bytes, static_cast(output_format_), static_cast(hr)); create_fail_count_++; } - if (create_fail_count_ == 5) { - spdlog::error("DeckLink-out: suppressing further CreateVideoFrame errors"); - } return; } diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index 4a369db..b08b675 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -55,6 +55,8 @@ private: IDeckLinkOutput* output_ = nullptr; BMDTimeValue frame_duration_ = 1000; BMDTimeScale time_scale_ = 50000; + BMDPixelFormat output_format_ = bmdFormat10BitYUV; + int preroll_frames_ = 3; std::atomic playing_{false}; int create_fail_count_ = 0; From bd1f060c73794b9ad23ef59cc89f4d08fd275bc9 Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:23:15 +0300 Subject: [PATCH 12/14] decklink-out: reuse frames via ScheduledFrameCompleted callback pool --- nodes/decklink-out/src/decklink_out_node.cpp | 32 +++++++++++++++----- nodes/decklink-out/src/decklink_out_node.hpp | 5 +++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index 4ff620a..2f6ed87 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -202,7 +202,9 @@ bool DeckLinkOutNode::open_device() { void DeckLinkOutNode::close_device() { if (output_) { - output_->StopScheduledPlayback(0, nullptr, time_scale_); + if (playing_) { + output_->StopScheduledPlayback(0, nullptr, time_scale_); + } output_->DisableVideoOutput(); output_->Release(); output_ = nullptr; @@ -213,6 +215,13 @@ void DeckLinkOutNode::close_device() { } playing_ = false; callback_.reset(); + { + std::lock_guard lock(frame_pool_mutex_); + for (auto* f : frame_pool_) { + f->Release(); + } + frame_pool_.clear(); + } spdlog::info("DeckLink-out: device closed"); } @@ -229,14 +238,15 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, } IDeckLinkMutableVideoFrame* frame = nullptr; - HRESULT hr = output_->CreateVideoFrame(width, height, out_row_bytes, output_format_, - bmdFrameFlagDefault, &frame); - if (hr != S_OK || !frame) { - if (create_fail_count_ < 5) { - spdlog::error("DeckLink-out: CreateVideoFrame {}x{} row_bytes={} fmt=0x{:x} hr=0x{:08x}", - width, height, out_row_bytes, static_cast(output_format_), static_cast(hr)); - create_fail_count_++; + { + std::unique_lock lock(frame_pool_mutex_); + if (frame_pool_cv_.wait_for(lock, std::chrono::milliseconds(100), [this] { return !frame_pool_.empty(); })) { + frame = frame_pool_.back(); + frame_pool_.pop_back(); } + } + + if (!frame) { return; } @@ -334,6 +344,12 @@ nlohmann::json DeckLinkOutNode::status() const { HRESULT DeckLinkOutNode::OutputCallback::ScheduledFrameCompleted( IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result) { + if (completedFrame) { + completedFrame->AddRef(); + std::lock_guard lock(owner_.frame_pool_mutex_); + owner_.frame_pool_.push_back(static_cast(completedFrame)); + owner_.frame_pool_cv_.notify_one(); + } return S_OK; } diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index b08b675..31298a0 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -7,9 +7,11 @@ #include #include +#include #include #include #include +#include namespace dmf_node { @@ -81,6 +83,9 @@ private: }; std::unique_ptr callback_; + std::vector frame_pool_; + std::mutex frame_pool_mutex_; + std::condition_variable frame_pool_cv_; IDeckLinkMutableVideoFrame* scheduled_frame_ = nullptr; }; From b434c61b86e6c8ce733476371639fa0aeb5f80b2 Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:24:54 +0300 Subject: [PATCH 13/14] decklink-out: non-blocking frame pool, skip late frames instead of realign, 5 preroll frames --- nodes/decklink-out/src/decklink_out_node.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index 2f6ed87..e770e10 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -157,7 +157,7 @@ bool DeckLinkOutNode::open_device() { is_interlaced_); } - preroll_frames_ = 3; + preroll_frames_ = 5; for (int i = 0; i < preroll_frames_; ++i) { IDeckLinkMutableVideoFrame* frame = nullptr; int32_t out_row_bytes = out_width_ * 16 / 6; @@ -239,8 +239,8 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, IDeckLinkMutableVideoFrame* frame = nullptr; { - std::unique_lock lock(frame_pool_mutex_); - if (frame_pool_cv_.wait_for(lock, std::chrono::milliseconds(100), [this] { return !frame_pool_.empty(); })) { + std::lock_guard lock(frame_pool_mutex_); + if (!frame_pool_.empty()) { frame = frame_pool_.back(); frame_pool_.pop_back(); } @@ -295,16 +295,18 @@ void DeckLinkOutNode::process() { uint8_t* payload = nullptr; auto status = mxlFlowReaderGetGrain(*reader_, source_index, 5000000ULL, &grain_info, &payload); if (status != MXL_STATUS_OK) { - if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE || status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { + if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE) { auto now = mxlGetTime(); auto current_index = mxlTimestampToIndex(&flow_rate_, now); - spdlog::warn("DeckLink-out: realigned from {} to {}", source_index, current_index - 2); + spdlog::warn("DeckLink-out: grain too late, skipping to index {}", current_index - 2); source_index = current_index - 2; - status = mxlFlowReaderGetGrain(*reader_, source_index, 5000000ULL, &grain_info, &payload); if (status != MXL_STATUS_OK) { + grains_read_++; return; } + } else if (status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { + return; } else { return; } @@ -320,7 +322,6 @@ void DeckLinkOutNode::process() { schedule_frame(payload, src_width, src_height, src_row_bytes); - read_index_ = source_index + 1; grains_read_++; if (grains_read_ == 1) { From 4402fb2e120bad0d9a046765a1f0636b0ba24e77 Mon Sep 17 00:00:00 2001 From: Johanness Date: Sat, 30 May 2026 23:26:51 +0300 Subject: [PATCH 14/14] fix decklink-out: use stream_frame_ counter for DeckLink scheduling instead of TAI-based grains_read_ --- nodes/decklink-out/src/decklink_out_node.cpp | 4 +++- nodes/decklink-out/src/decklink_out_node.hpp | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nodes/decklink-out/src/decklink_out_node.cpp b/nodes/decklink-out/src/decklink_out_node.cpp index e770e10..9e55119 100644 --- a/nodes/decklink-out/src/decklink_out_node.cpp +++ b/nodes/decklink-out/src/decklink_out_node.cpp @@ -190,6 +190,7 @@ bool DeckLinkOutNode::open_device() { frame->Release(); } + stream_frame_ = preroll_frames_; if (output_->StartScheduledPlayback(0, time_scale_, 1.0) != S_OK) { spdlog::error("DeckLink-out: failed to start scheduled playback"); return false; @@ -267,9 +268,10 @@ void DeckLinkOutNode::schedule_frame(void* mxl_payload, long width, long height, buf->Release(); } - auto stream_time = grains_read_ * frame_duration_; + auto stream_time = stream_frame_ * frame_duration_; output_->ScheduleVideoFrame(frame, stream_time, frame_duration_, time_scale_); frame->Release(); + stream_frame_++; } void DeckLinkOutNode::process() { diff --git a/nodes/decklink-out/src/decklink_out_node.hpp b/nodes/decklink-out/src/decklink_out_node.hpp index 31298a0..192c5fa 100644 --- a/nodes/decklink-out/src/decklink_out_node.hpp +++ b/nodes/decklink-out/src/decklink_out_node.hpp @@ -62,6 +62,7 @@ private: std::atomic playing_{false}; int create_fail_count_ = 0; + uint64_t stream_frame_ = 0; bool is_interlaced_ = false; long out_width_ = 1920; long out_height_ = 1080;