hardcoded decklink mode fix

This commit is contained in:
Johanness
2026-05-28 23:43:26 +03:00
parent 96bef6cd32
commit 34f72c22f0
12 changed files with 222 additions and 35 deletions
+1
View File
@@ -28,6 +28,7 @@ find_package(nlohmann_json CONFIG REQUIRED)
find_package(Libwebsockets CONFIG REQUIRED) find_package(Libwebsockets CONFIG REQUIRED)
find_package(Catch2 CONFIG QUIET) find_package(Catch2 CONFIG QUIET)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(extern/mxl) add_subdirectory(extern/mxl)
add_subdirectory(libs/dmf-node) add_subdirectory(libs/dmf-node)
add_subdirectory(libs/dmf-engine) add_subdirectory(libs/dmf-engine)
+66
View File
@@ -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"
Vendored
+1 -1
View File
@@ -1 +1 @@
../../mxl ../mxl
@@ -9,6 +9,7 @@ namespace dmf_engine {
class NodeControlClient { class NodeControlClient {
public: public:
bool send_command(uint16_t port, const std::string& json_cmd); 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 register_node(const std::string& node_id, uint16_t port);
void unregister_node(const std::string& node_id); void unregister_node(const std::string& node_id);
+22 -1
View File
@@ -254,8 +254,29 @@ static void handle_request(const std::string& method, const std::string& path,
return; 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_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; nlohmann::json cmd;
cmd["cmd"] = "add_writer"; cmd["cmd"] = "add_writer";
@@ -85,4 +85,60 @@ bool NodeControlClient::send_command(uint16_t port, const std::string& json_cmd)
return ok; 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<struct sockaddr*>(&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<ssize_t>(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 } // namespace dmf_engine
@@ -11,7 +11,7 @@ struct lws;
namespace dmf_node { namespace dmf_node {
using CommandHandler = std::function<void(const nlohmann::json& payload)>; using CommandHandler = std::function<nlohmann::json(const nlohmann::json& payload)>;
using StatusCallback = std::function<void(const nlohmann::json& event)>; using StatusCallback = std::function<void(const nlohmann::json& event)>;
class ControlServer { class ControlServer {
+6 -5
View File
@@ -29,17 +29,18 @@ struct ControlServer::Impl {
struct lws_context* context = nullptr; 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")) { if (!msg.contains("cmd")) {
spdlog::warn("Control: message missing 'cmd' field"); spdlog::warn("Control: message missing 'cmd' field");
return; return {{"error", "missing 'cmd' field"}};
} }
auto cmd = msg["cmd"].get<std::string>(); auto cmd = msg["cmd"].get<std::string>();
auto it = data->commands.find(cmd); auto it = data->commands.find(cmd);
if (it != data->commands.end()) { if (it != data->commands.end()) {
it->second(msg); return it->second(msg);
} else { } else {
spdlog::warn("Control: unknown command '{}'", cmd); 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 { try {
auto msg = nlohmann::json::parse(ps->http_body); auto msg = nlohmann::json::parse(ps->http_body);
dispatch_command(ps->data, msg); auto result = dispatch_command(ps->data, msg);
return send_http_json(wsi, "200 OK", R"({"ok":true})"); return send_http_json(wsi, "200 OK", result.dump());
} catch (const nlohmann::json::parse_error& e) { } catch (const nlohmann::json::parse_error& e) {
return send_http_json(wsi, "400 Bad Request", return send_http_json(wsi, "400 Bad Request",
nlohmann::json({{"error", e.what()}}).dump()); nlohmann::json({{"error", e.what()}}).dump());
+16 -9
View File
@@ -101,7 +101,7 @@ int NodeRunner::exec(std::unique_ptr<Node> node) {
}; };
std::vector<FlowResource> flow_resources; std::vector<FlowResource> 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<std::string>(); auto flow_id = msg["flow_id"].get<std::string>();
auto port_id = msg["port_id"].get<std::string>(); auto port_id = msg["port_id"].get<std::string>();
auto flow_def = msg["flow_def"].dump(); auto flow_def = msg["flow_def"].dump();
@@ -112,14 +112,15 @@ int NodeRunner::exec(std::unique_ptr<Node> node) {
auto status = mxlCreateFlowWriter(mxl_instance_, flow_def.c_str(), nullptr, &writer, &config_info, &created); auto status = mxlCreateFlowWriter(mxl_instance_, flow_def.c_str(), nullptr, &writer, &config_info, &created);
if (status != MXL_STATUS_OK || !writer) { if (status != MXL_STATUS_OK || !writer) {
spdlog::error("Failed to create flow writer for flow {}: status={}", flow_id, static_cast<int>(status)); spdlog::error("Failed to create flow writer for flow {}: status={}", flow_id, static_cast<int>(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); spdlog::info("Created flow writer on port '{}' flow {} (created={})", port_id, flow_id, created);
flow_resources.push_back({port_id, writer, nullptr}); flow_resources.push_back({port_id, writer, nullptr});
node->on_add_writer(port_id, writer); 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<std::string>(); auto flow_id = msg["flow_id"].get<std::string>();
auto port_id = msg["port_id"].get<std::string>(); auto port_id = msg["port_id"].get<std::string>();
@@ -127,14 +128,15 @@ int NodeRunner::exec(std::unique_ptr<Node> node) {
auto status = mxlCreateFlowReader(mxl_instance_, flow_id.c_str(), nullptr, &reader); auto status = mxlCreateFlowReader(mxl_instance_, flow_id.c_str(), nullptr, &reader);
if (status != MXL_STATUS_OK || !reader) { if (status != MXL_STATUS_OK || !reader) {
spdlog::error("Failed to create flow reader for flow {}: status={}", flow_id, static_cast<int>(status)); spdlog::error("Failed to create flow reader for flow {}: status={}", flow_id, static_cast<int>(status));
return; return {{"ok", false}, {"error", "failed to create flow reader"}};
} }
spdlog::info("Created flow reader on port '{}' flow {}", port_id, flow_id); spdlog::info("Created flow reader on port '{}' flow {}", port_id, flow_id);
flow_resources.push_back({port_id, nullptr, reader}); flow_resources.push_back({port_id, nullptr, reader});
node->on_add_reader(port_id, 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<std::string>(); auto port_id = msg["port_id"].get<std::string>();
node->on_remove_writer(port_id); node->on_remove_writer(port_id);
for (auto it = flow_resources.begin(); it != flow_resources.end(); ++it) { for (auto it = flow_resources.begin(); it != flow_resources.end(); ++it) {
@@ -145,9 +147,10 @@ int NodeRunner::exec(std::unique_ptr<Node> node) {
} }
} }
spdlog::info("Removed writer on port '{}'", port_id); 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<std::string>(); auto port_id = msg["port_id"].get<std::string>();
node->on_remove_reader(port_id); node->on_remove_reader(port_id);
for (auto it = flow_resources.begin(); it != flow_resources.end(); ++it) { for (auto it = flow_resources.begin(); it != flow_resources.end(); ++it) {
@@ -158,26 +161,30 @@ int NodeRunner::exec(std::unique_ptr<Node> node) {
} }
} }
spdlog::info("Removed reader on port '{}'", port_id); 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")) { if (msg.contains("params")) {
node->configure(msg["params"]); node->configure(msg["params"]);
spdlog::info("Reconfigured node '{}'", node_id_); 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; nlohmann::json resp;
resp["event"] = "status"; resp["event"] = "status";
resp["node_id"] = node_id_; resp["node_id"] = node_id_;
resp["data"] = node->status(); resp["data"] = node->status();
control_server->send_event(resp); 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"); spdlog::info("Shutdown command received");
g_running = false; g_running = false;
return {{"ok", true}};
}); });
nlohmann::json ready_event; nlohmann::json ready_event;
+39 -12
View File
@@ -32,6 +32,10 @@ void DeckLinkInNode::configure(const nlohmann::json& params) {
spdlog::warn("DeckLink-in: unknown mode '{}', defaulting to 1080i50", mode_str); 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) { 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); spdlog::info("DeckLink-in: writer added, grain_rate={}/{}", grain_rate_.numerator, grain_rate_.denominator);
if (!open_device()) { if (input_ && !capturing_) {
spdlog::error("DeckLink-in: failed to open device"); if (input_->StartStreams() != S_OK) {
return; 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; BMDTimeScale scale = 0;
mode->GetFrameRate(&duration, &scale); mode->GetFrameRate(&duration, &scale);
mode->Release(); 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<int32_t>(2 * scale);
grain_rate_.denominator = static_cast<int32_t>(duration);
} else {
grain_rate_.numerator = static_cast<int32_t>(scale);
grain_rate_.denominator = static_cast<int32_t>(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::info("DeckLink-in: device opened, waiting for writer to start capture");
spdlog::error("DeckLink-in: failed to start streams");
return false;
}
capturing_ = true;
spdlog::info("DeckLink-in: capture started");
return true; return true;
} }
void DeckLinkInNode::close_device() { void DeckLinkInNode::close_device() {
if (input_) { if (input_) {
input_->StopStreams(); if (capturing_) {
input_->StopStreams();
}
input_->DisableVideoInput(); input_->DisableVideoInput();
input_->Release(); input_->Release();
input_ = nullptr; input_ = nullptr;
@@ -199,6 +222,10 @@ nlohmann::json DeckLinkInNode::status() const {
{"write_index", write_index_}, {"write_index", write_index_},
{"capturing", capturing_.load()}, {"capturing", capturing_.load()},
{"has_writer", writer_.has_value()}, {"has_writer", writer_.has_value()},
{"grain_rate", {{"numerator", grain_rate_.numerator}, {"denominator", grain_rate_.denominator}}},
{"width", frame_width_},
{"height", frame_height_},
{"interlaced", is_interlaced_},
}; };
} }
+4 -3
View File
@@ -40,7 +40,7 @@ private:
void close_device(); void close_device();
std::optional<mxlFlowWriter> writer_; std::optional<mxlFlowWriter> writer_;
mxlRational grain_rate_{50, 1}; mxlRational grain_rate_{25, 1};
uint64_t write_index_ = 0; uint64_t write_index_ = 0;
uint64_t grains_written_ = 0; uint64_t grains_written_ = 0;
@@ -51,11 +51,12 @@ private:
IDeckLinkInput* input_ = nullptr; IDeckLinkInput* input_ = nullptr;
std::atomic<bool> capturing_{false}; std::atomic<bool> capturing_{false};
bool is_interlaced_ = false;
std::mutex frame_mutex_; std::mutex frame_mutex_;
void* frame_data_ = nullptr; void* frame_data_ = nullptr;
long frame_row_bytes_ = 0; long frame_row_bytes_ = 0;
long frame_width_ = 0; long frame_width_ = 1920;
long frame_height_ = 0; long frame_height_ = 1080;
bool frame_ready_ = false; bool frame_ready_ = false;
class CaptureCallback : public IDeckLinkInputCallback { class CaptureCallback : public IDeckLinkInputCallback {
+9 -3
View File
@@ -201,10 +201,15 @@ void DeckLinkOutNode::process() {
mxlFlowConfigInfo config{}; mxlFlowConfigInfo config{};
mxlFlowReaderGetConfigInfo(*reader_, &config); mxlFlowReaderGetConfigInfo(*reader_, &config);
auto grain_size = grain_info.grainSize; 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<long>(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; read_index_ = grain_info.index + 1;
grains_read_++; grains_read_++;
@@ -221,6 +226,7 @@ nlohmann::json DeckLinkOutNode::status() const {
{"read_index", read_index_}, {"read_index", read_index_},
{"playing", playing_.load()}, {"playing", playing_.load()},
{"has_reader", reader_.has_value()}, {"has_reader", reader_.has_value()},
{"grain_rate", {{"numerator", grain_rate_.numerator}, {"denominator", grain_rate_.denominator}}},
}; };
} }