#include "decklink_out_node.hpp" #include #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); } } 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) { 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(*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(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) { 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(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(&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(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