Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4402fb2e12 | |||
| b434c61b86 | |||
| bd1f060c73 | |||
| 26febc41d6 | |||
| a3cd45cc49 | |||
| b4f766fb9d | |||
| b5d9c7cd3f | |||
| c2e823beda | |||
| 003279669e | |||
| 2206fd71b3 | |||
| 8b5fb3dc36 | |||
| 34f72c22f0 | |||
| 96bef6cd32 | |||
| d02223224f | |||
| db41a91967 |
@@ -13,6 +13,14 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
|||||||
|
|
||||||
option(DMF_BUILD_TESTS "Build tests" ON)
|
option(DMF_BUILD_TESTS "Build tests" ON)
|
||||||
option(DMF_BUILD_MXL_TOOLS "Build MXL tools (testsrc, sink)" OFF)
|
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(fmt CONFIG REQUIRED)
|
||||||
find_package(spdlog CONFIG REQUIRED)
|
find_package(spdlog CONFIG REQUIRED)
|
||||||
@@ -20,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)
|
||||||
|
|||||||
Executable
+66
@@ -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
@@ -1 +1 @@
|
|||||||
/home/itten/DMF/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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
#include "decklink_in_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 {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.contains("input_connection")) {
|
||||||
|
auto conn_str = params["input_connection"].get<std::string>();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (input_ && !capturing_) {
|
||||||
|
if (input_->StartStreams() != S_OK) {
|
||||||
|
spdlog::error("DeckLink-in: failed to start streams");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
capturing_ = true;
|
||||||
|
spdlog::info("DeckLink-in: capture started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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();
|
||||||
|
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();
|
||||||
|
|
||||||
|
is_interlaced_ = (display_mode_ == bmdModeHD1080i50 ||
|
||||||
|
display_mode_ == bmdModeHD1080i5994);
|
||||||
|
|
||||||
|
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_);
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info("DeckLink-in: device opened, waiting for writer to start capture");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkInNode::close_device() {
|
||||||
|
if (input_) {
|
||||||
|
if (capturing_) {
|
||||||
|
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_data_) {
|
||||||
|
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);
|
||||||
|
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()},
|
||||||
|
{"grain_rate", {{"numerator", grain_rate_.numerator}, {"denominator", grain_rate_.denominator}}},
|
||||||
|
{"width", frame_width_},
|
||||||
|
{"height", frame_height_},
|
||||||
|
{"interlaced", is_interlaced_},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,84 @@
|
|||||||
|
#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_{25, 1};
|
||||||
|
uint64_t write_index_ = 0;
|
||||||
|
uint64_t grains_written_ = 0;
|
||||||
|
|
||||||
|
int device_index_ = 0;
|
||||||
|
BMDDisplayMode display_mode_ = bmdModeHD1080i50;
|
||||||
|
BMDVideoConnection input_connection_ = bmdVideoConnectionUnspecified;
|
||||||
|
|
||||||
|
IDeckLink* decklink_ = nullptr;
|
||||||
|
IDeckLinkInput* input_ = nullptr;
|
||||||
|
|
||||||
|
std::atomic<bool> capturing_{false};
|
||||||
|
bool is_interlaced_ = false;
|
||||||
|
std::mutex frame_mutex_;
|
||||||
|
void* frame_data_ = nullptr;
|
||||||
|
long frame_row_bytes_ = 0;
|
||||||
|
long frame_width_ = 1920;
|
||||||
|
long frame_height_ = 1080;
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
#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_);
|
||||||
|
}
|
||||||
|
|
||||||
|
preroll_frames_ = 5;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
playing_ = true;
|
||||||
|
spdlog::info("DeckLink-out: playback started");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkOutNode::close_device() {
|
||||||
|
if (output_) {
|
||||||
|
if (playing_) {
|
||||||
|
output_->StopScheduledPlayback(0, nullptr, time_scale_);
|
||||||
|
}
|
||||||
|
output_->DisableVideoOutput();
|
||||||
|
output_->Release();
|
||||||
|
output_ = nullptr;
|
||||||
|
}
|
||||||
|
if (decklink_) {
|
||||||
|
decklink_->Release();
|
||||||
|
decklink_ = nullptr;
|
||||||
|
}
|
||||||
|
playing_ = false;
|
||||||
|
callback_.reset();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(frame_pool_mutex_);
|
||||||
|
for (auto* f : frame_pool_) {
|
||||||
|
f->Release();
|
||||||
|
}
|
||||||
|
frame_pool_.clear();
|
||||||
|
}
|
||||||
|
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_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;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(frame_pool_mutex_);
|
||||||
|
if (!frame_pool_.empty()) {
|
||||||
|
frame = frame_pool_.back();
|
||||||
|
frame_pool_.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!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<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 = stream_frame_ * frame_duration_;
|
||||||
|
output_->ScheduleVideoFrame(frame, stream_time, frame_duration_, time_scale_);
|
||||||
|
frame->Release();
|
||||||
|
stream_frame_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkOutNode::process() {
|
||||||
|
if (!reader_ || !playing_) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
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);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
auto now = mxlGetTime();
|
||||||
|
auto current_index = mxlTimestampToIndex(&flow_rate_, now);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (completedFrame) {
|
||||||
|
completedFrame->AddRef();
|
||||||
|
std::lock_guard<std::mutex> lock(owner_.frame_pool_mutex_);
|
||||||
|
owner_.frame_pool_.push_back(static_cast<IDeckLinkMutableVideoFrame*>(completedFrame));
|
||||||
|
owner_.frame_pool_cv_.notify_one();
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT DeckLinkOutNode::OutputCallback::ScheduledPlaybackHasStopped() {
|
||||||
|
owner_.playing_ = false;
|
||||||
|
spdlog::info("DeckLink-out: scheduled playback stopped");
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dmf_node
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <dmf-node/node.hpp>
|
||||||
|
#include <mxl/flow.h>
|
||||||
|
#include <mxl/time.h>
|
||||||
|
|
||||||
|
#include <DeckLinkAPI.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dmf_node {
|
||||||
|
|
||||||
|
class DeckLinkOutNode : public Node {
|
||||||
|
public:
|
||||||
|
DeckLinkOutNode() = default;
|
||||||
|
~DeckLinkOutNode();
|
||||||
|
|
||||||
|
std::string type() const override { return "decklink-out"; }
|
||||||
|
|
||||||
|
std::vector<PortDef> input_ports() const override {
|
||||||
|
return {{"video_in", PortDirection::Input, MediaType::VideoV210}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PortDef> 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<mxlFlowReader> reader_;
|
||||||
|
mxlRational flow_rate_{50, 1};
|
||||||
|
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;
|
||||||
|
BMDVideoConnection output_connection_ = bmdVideoConnectionUnspecified;
|
||||||
|
|
||||||
|
IDeckLink* decklink_ = nullptr;
|
||||||
|
IDeckLinkOutput* output_ = nullptr;
|
||||||
|
BMDTimeValue frame_duration_ = 1000;
|
||||||
|
BMDTimeScale time_scale_ = 50000;
|
||||||
|
BMDPixelFormat output_format_ = bmdFormat10BitYUV;
|
||||||
|
int preroll_frames_ = 3;
|
||||||
|
|
||||||
|
std::atomic<bool> playing_{false};
|
||||||
|
int create_fail_count_ = 0;
|
||||||
|
uint64_t stream_frame_ = 0;
|
||||||
|
bool is_interlaced_ = false;
|
||||||
|
long out_width_ = 1920;
|
||||||
|
long out_height_ = 1080;
|
||||||
|
|
||||||
|
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<OutputCallback> callback_;
|
||||||
|
std::vector<IDeckLinkMutableVideoFrame*> frame_pool_;
|
||||||
|
std::mutex frame_pool_mutex_;
|
||||||
|
std::condition_variable frame_pool_cv_;
|
||||||
|
IDeckLinkMutableVideoFrame* scheduled_frame_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace dmf_node
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#include <dmf-node/node_runner.hpp>
|
||||||
|
#include "decklink_out_node.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
return dmf_node::NodeRunner::run<dmf_node::DeckLinkOutNode>(argc, argv);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user