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
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#include "decklink_in_node.hpp"
|
||||
|
||||
#include <mxl/flow.h>
|
||||
#include <mxl/mxl.h>
|
||||
#include <mxl/time.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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<int>();
|
||||
}
|
||||
if (params.contains("mode")) {
|
||||
auto mode_str = params["mode"].get<std::string>();
|
||||
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<CaptureCallback>(*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<std::mutex> 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<long>(dst_row_bytes), src_row_bytes);
|
||||
|
||||
for (long y = 0; y < src_height && y < static_cast<long>(out_grain.grainSize / dst_row_bytes); ++y) {
|
||||
std::memcpy(out_payload + y * dst_row_bytes,
|
||||
static_cast<uint8_t*>(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<std::mutex> 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
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <dmf-node/node.hpp>
|
||||
#include <mxl/flow.h>
|
||||
#include <mxl/time.h>
|
||||
|
||||
#include <DeckLinkAPI.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
namespace dmf_node {
|
||||
|
||||
class DeckLinkInNode : public Node {
|
||||
public:
|
||||
DeckLinkInNode() = default;
|
||||
~DeckLinkInNode();
|
||||
|
||||
std::string type() const override { return "decklink-in"; }
|
||||
|
||||
std::vector<PortDef> input_ports() const override { return {}; }
|
||||
|
||||
std::vector<PortDef> 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<mxlFlowWriter> 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<bool> 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<CaptureCallback> callback_;
|
||||
};
|
||||
|
||||
} // namespace dmf_node
|
||||
@@ -0,0 +1,6 @@
|
||||
#include <dmf-node/node_runner.hpp>
|
||||
#include "decklink_in_node.hpp"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
return dmf_node::NodeRunner::run<dmf_node::DeckLinkInNode>(argc, argv);
|
||||
}
|
||||
Reference in New Issue
Block a user