Initial commit: testpattern → fakesink pipeline over MXL shared memory
- NodeBase, Signal, FlowDef, V210 shared headers - testpattern node: SMPTE 75% color bars writer at 25fps - fakesink node: non-blocking MXL reader with per-second stats - studio-manager: FlowGraph data model, graph-driven fork/exec launcher - mxl pinned as submodule at 0ae1dc5 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace dmf {
|
||||
|
||||
// Generates a minimal but valid NMOS IS-04 flow definition JSON string
|
||||
// for a video/v210 flow. MXL uses this to set up the ring buffer geometry.
|
||||
inline std::string make_video_flow_def(
|
||||
const std::string& flow_id,
|
||||
const std::string& label,
|
||||
int width, int height,
|
||||
int fps_num, int fps_den = 1)
|
||||
{
|
||||
using json = nlohmann::json;
|
||||
return json{
|
||||
{"id", flow_id},
|
||||
{"format", "urn:x-nmos:format:video"},
|
||||
{"label", label},
|
||||
{"description", label + " MXL Video Flow"},
|
||||
{"media_type", "video/v210"},
|
||||
{"parents", json::array()},
|
||||
{"grain_rate", {{"numerator", fps_num}, {"denominator", fps_den}}},
|
||||
{"frame_width", width},
|
||||
{"frame_height", height},
|
||||
{"interlace_mode", "progressive"},
|
||||
{"colorspace", "BT709"},
|
||||
{"tags", {
|
||||
{"urn:x-nmos:tag:grouphint/v1.0", json::array({label + ":Video"})}
|
||||
}},
|
||||
{"components", json::array({
|
||||
{{"name","Y"}, {"width",width}, {"height",height}, {"bit_depth",10}},
|
||||
{{"name","Cb"}, {"width",width/2}, {"height",height}, {"bit_depth",10}},
|
||||
{{"name","Cr"}, {"width",width/2}, {"height",height}, {"bit_depth",10}}
|
||||
})}
|
||||
}.dump();
|
||||
}
|
||||
|
||||
} // namespace dmf
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <mxl/mxl.h>
|
||||
#include "Signal.hpp"
|
||||
|
||||
namespace dmf {
|
||||
|
||||
// Base class for all DMF node binaries.
|
||||
//
|
||||
// Handles the boilerplate every node needs:
|
||||
// - NODE_CONFIG env var → parsed JSON config
|
||||
// - MXL_DOMAIN env var → domain path
|
||||
// - mxlCreateInstance / mxlDestroyInstance lifecycle
|
||||
// - Signal handler installation
|
||||
// - [node_id]-prefixed logging
|
||||
//
|
||||
// Derived class implements only run(), which receives a valid MXL instance
|
||||
// and runs until dmf::g_running is false (set by SIGTERM/SIGINT).
|
||||
class NodeBase {
|
||||
public:
|
||||
virtual ~NodeBase() = default;
|
||||
|
||||
// Entry point for main(). Returns 0 on success, 1 on error.
|
||||
int execute() {
|
||||
install_signal_handlers();
|
||||
|
||||
const char* config_env = std::getenv("NODE_CONFIG");
|
||||
if (!config_env) {
|
||||
fprintf(stderr, "[node] NODE_CONFIG not set\n");
|
||||
return 1;
|
||||
}
|
||||
cfg_ = nlohmann::json::parse(config_env, nullptr, /*allow_exceptions=*/false);
|
||||
if (cfg_.is_discarded()) {
|
||||
fprintf(stderr, "[node] NODE_CONFIG is not valid JSON\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
node_id_ = cfg_.value("node_id", std::string("node"));
|
||||
|
||||
const char* domain_env = std::getenv("MXL_DOMAIN");
|
||||
#ifdef __APPLE__
|
||||
domain_ = domain_env ? domain_env : "/tmp/dmf-studio";
|
||||
#else
|
||||
domain_ = domain_env ? domain_env : "/dev/shm/dmf-studio";
|
||||
#endif
|
||||
|
||||
log("domain=%s", domain_.c_str());
|
||||
|
||||
inst_ = mxlCreateInstance(domain_.c_str(), nullptr);
|
||||
if (!inst_) {
|
||||
log("mxlCreateInstance failed at %s", domain_.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
mxlDestroyInstance(inst_);
|
||||
inst_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Implement the node's processing loop here.
|
||||
// Create MXL writers/readers, loop while dmf::g_running, release them before returning.
|
||||
virtual void run() = 0;
|
||||
|
||||
const nlohmann::json& config() const { return cfg_; }
|
||||
mxlInstance instance() const { return inst_; }
|
||||
const std::string& node_id() const { return node_id_; }
|
||||
const std::string& domain() const { return domain_; }
|
||||
|
||||
// Printf-style log with automatic "[node_id] " prefix and trailing newline.
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__((format(printf, 2, 3)))
|
||||
#endif
|
||||
void log(const char* fmt, ...) const {
|
||||
fprintf(stderr, "[%s] ", node_id_.c_str());
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vfprintf(stderr, fmt, args);
|
||||
va_end(args);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
private:
|
||||
nlohmann::json cfg_;
|
||||
mxlInstance inst_{nullptr};
|
||||
std::string node_id_{"node"};
|
||||
std::string domain_;
|
||||
};
|
||||
|
||||
} // namespace dmf
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
|
||||
namespace dmf {
|
||||
|
||||
inline std::atomic<bool> g_running{true};
|
||||
|
||||
namespace detail {
|
||||
inline void on_signal(int) noexcept {
|
||||
g_running.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
inline void install_signal_handlers() {
|
||||
std::signal(SIGTERM, detail::on_signal);
|
||||
std::signal(SIGINT, detail::on_signal);
|
||||
}
|
||||
|
||||
} // namespace dmf
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace dmf::v210 {
|
||||
|
||||
// SMPTE 75% color bars — 10-bit limited range
|
||||
// Y: 64 (black) to 940 (white)
|
||||
// Cb/Cr: 64 to 960, 512 = neutral grey
|
||||
struct Color { uint16_t y, cb, cr; };
|
||||
|
||||
constexpr std::array<Color, 7> SMPTE_BARS = {{
|
||||
{721, 512, 512}, // white
|
||||
{674, 176, 543}, // yellow
|
||||
{581, 589, 176}, // cyan
|
||||
{534, 253, 207}, // green
|
||||
{251, 771, 817}, // magenta
|
||||
{204, 435, 848}, // red
|
||||
{111, 848, 481}, // blue
|
||||
}};
|
||||
|
||||
// Pack 6 pixels into 4 x 32-bit V210 words (16 bytes total).
|
||||
//
|
||||
// V210 is 4:2:2 — each pair of pixels shares one Cb and one Cr sample.
|
||||
// The three pairs in a block map to words like this (bits [9:0],[19:10],[29:20]):
|
||||
// word 0: Cb(pair0) | Y(px0) | Cr(pair0)
|
||||
// word 1: Y(px1) | Cb(pair1) | Y(px2)
|
||||
// word 2: Cr(pair1) | Y(px3) | Cb(pair2)
|
||||
// word 3: Y(px4) | Cr(pair2) | Y(px5)
|
||||
inline void pack_block(
|
||||
uint8_t* out,
|
||||
Color p01, uint16_t y0, uint16_t y1, // pair 0–1
|
||||
Color p23, uint16_t y2, uint16_t y3, // pair 2–3
|
||||
Color p45, uint16_t y4, uint16_t y5) // pair 4–5
|
||||
{
|
||||
auto* w = reinterpret_cast<uint32_t*>(out);
|
||||
w[0] = (p01.cb & 0x3FFu) | ((y0 & 0x3FFu) << 10) | ((p01.cr & 0x3FFu) << 20);
|
||||
w[1] = (y1 & 0x3FFu) | ((p23.cb & 0x3FFu) << 10) | ((y2 & 0x3FFu) << 20);
|
||||
w[2] = (p23.cr & 0x3FFu) | ((y3 & 0x3FFu) << 10) | ((p45.cb & 0x3FFu) << 20);
|
||||
w[3] = (y4 & 0x3FFu) | ((p45.cr & 0x3FFu) << 10) | ((y5 & 0x3FFu) << 20);
|
||||
}
|
||||
|
||||
// Write one horizontal line of SMPTE 75% bars.
|
||||
// `stride` is the line size in bytes as returned by MXL (configInfo.discrete.sliceSizes[0]).
|
||||
// Bytes beyond the active pixels are already zeroed by the mmap, so no explicit padding needed.
|
||||
inline void write_bar_line(uint8_t* line, int width, uint32_t /*stride*/)
|
||||
{
|
||||
const int n = static_cast<int>(SMPTE_BARS.size());
|
||||
const int blocks = width / 6; // one V210 block = 6 pixels = 16 bytes
|
||||
|
||||
for (int b = 0; b < blocks; b++) {
|
||||
int x = b * 6;
|
||||
auto color = [&](int px) -> const Color& {
|
||||
return SMPTE_BARS[static_cast<size_t>(px * n / width)];
|
||||
};
|
||||
const Color& c01 = color(x);
|
||||
const Color& c23 = color(x + 2);
|
||||
const Color& c45 = color(x + 4);
|
||||
pack_block(line + b * 16,
|
||||
c01, c01.y, color(x+1).y,
|
||||
c23, c23.y, color(x+3).y,
|
||||
c45, c45.y, color(x+5).y);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill an entire frame buffer with SMPTE 75% color bars.
|
||||
inline void fill_frame(uint8_t* buf, int width, int height, uint32_t stride)
|
||||
{
|
||||
for (int y = 0; y < height; y++) {
|
||||
write_bar_line(buf + static_cast<ptrdiff_t>(y) * stride, width, stride);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dmf::v210
|
||||
Reference in New Issue
Block a user