294 lines
10 KiB
C++
294 lines
10 KiB
C++
#include "decklink_out_node.hpp"
|
|
|
|
#include <mxl/flow.h>
|
|
#include <mxl/mxl.h>
|
|
#include <mxl/time.h>
|
|
|
|
#include <DeckLinkAPIConfiguration.h>
|
|
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <cstring>
|
|
|
|
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<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-out: unknown mode '{}', defaulting to 1080i50", mode_str);
|
|
}
|
|
}
|
|
if (params.contains("output_connection")) {
|
|
auto conn_str = params["output_connection"].get<std::string>();
|
|
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) {
|
|
if (port_id == "video_in") {
|
|
reader_ = reader;
|
|
|
|
mxlFlowConfigInfo config{};
|
|
mxlFlowReaderGetConfigInfo(*reader_, &config);
|
|
flow_rate_ = config.common.grainRate;
|
|
|
|
auto now = mxlGetTime();
|
|
auto current_index = mxlTimestampToIndex(&flow_rate_, now);
|
|
read_index_ = current_index - 2;
|
|
|
|
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");
|
|
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;
|
|
|
|
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<OutputCallback>(*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) {
|
|
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<int32_t>(scale);
|
|
output_rate_.denominator = static_cast<int32_t>(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) {
|
|
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;
|
|
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 {}x{}", width, height);
|
|
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<long>(out_row_bytes), row_bytes);
|
|
for (long y = 0; y < height; ++y) {
|
|
std::memcpy(static_cast<uint8_t*>(dst) + y * out_row_bytes,
|
|
static_cast<uint8_t*>(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(&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_, 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(&flow_rate_, now);
|
|
read_index_ = current_index - 2;
|
|
if (grains_read_ == 0) {
|
|
spdlog::warn("DeckLink-out: realigned to source index {}", read_index_);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
mxlFlowConfigInfo config{};
|
|
mxlFlowReaderGetConfigInfo(*reader_, &config);
|
|
auto grain_size = grain_info.grainSize;
|
|
|
|
long src_row_bytes = (config.discrete.sliceSizes[0] > 0) ? static_cast<long>(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, src_width, src_height, src_row_bytes);
|
|
|
|
read_index_ = source_index + 1;
|
|
grains_read_++;
|
|
|
|
if (grains_read_ == 1) {
|
|
spdlog::info("DeckLink-out: first grain output, source_index={}", source_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()},
|
|
{"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_},
|
|
};
|
|
}
|
|
|
|
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
|