How to Define and Use Custom FrameMetadata Types in ApraPipes: A Complete Guide
To define custom FrameMetadata types in ApraPipes, subclass FrameMetadata, implement virtual methods like reset() and isSet(), add a nested Metadata struct with registration constants, and use the REGISTER_FRAME_TYPE macro to enroll the type in the global registry.
ApraPipes treats every data packet flowing through a pipeline as a frame. Each frame carries a pointer to a FrameMetadata object that describes the data type, memory layout, and size. While the core library provides built-in metadata classes like RawImageMetadata and H264Metadata in base/include/RawImageMetadata.h and base/include/H264Metadata.h, you will often need to attach application-specific context—such as sensor IDs, GPS coordinates, or detection scores—that these built-in types cannot hold. Creating custom FrameMetadata types allows you to enrich frame data while maintaining full compatibility with the declarative pipeline system and runtime type safety.
Understanding the FrameMetadata Architecture
The Base FrameMetadata Class
All metadata in ApraPipes inherits from apra::FrameMetadata, defined in base/include/FrameMetadata.h. This abstract base class establishes the interface that every metadata type must implement. The core responsibilities include tracking whether the metadata has been initialized (isSet()), resetting fields to a default state (reset()), and optionally reporting the size of associated binary data (getDataSize()).
When you create a custom type, you must override these virtual methods to ensure the pipeline can correctly manage memory and validate frame state.
Built-in Metadata Types as Reference
The ApraPipes source code provides excellent reference implementations in base/include/RawImageMetadata.h and base/include/H264Metadata.h. These classes demonstrate how to:
- Inherit from
FrameMetadataand call the base constructor with an appropriateFrameType. - Add domain-specific fields (e.g.,
width,height,direction,mp4Seek). - Implement
reset()to clear custom fields back to sentinel values likeNOT_SET_NUM. - Provide convenience getters and setters for type-safe access.
Studying these files before writing your own implementation ensures you follow the established patterns for memory management and field initialization.
Step-by-Step Guide to Creating Custom FrameMetadata Types
Step 1: Subclass FrameMetadata and Implement Virtual Methods
Create a new header file for your metadata class and inherit from FrameMetadata. You must implement reset() and isSet() to satisfy the interface. If your metadata will carry binary data, also implement getDataSize().
// SensorMetadata.h
#pragma once
#include "FrameMetadata.h"
class SensorMetadata : public FrameMetadata {
public:
// Default constructor for registration
SensorMetadata() : FrameMetadata(FrameType::GENERAL) {}
// Parameterized constructor for actual use
SensorMetadata(int w, int h, double ts, int id)
: FrameMetadata(FrameType::GENERAL),
width(w), height(h), timestamp(ts), sensorId(id) {}
// Required: Reset all fields to default state
void reset() override {
FrameMetadata::reset();
width = NOT_SET_NUM;
height = NOT_SET_NUM;
timestamp = 0.0;
sensorId = -1;
}
// Required: Check if metadata has been initialized
bool isSet() override {
return width != NOT_SET_NUM;
}
// Optional: Report size of binary data if applicable
size_t getDataSize() override {
return sizeof(width) + sizeof(height) +
sizeof(timestamp) + sizeof(sensorId);
}
// Custom getters
int getWidth() const { return width; }
int getHeight() const { return height; }
double getTimestamp() const { return timestamp; }
int getSensorId() const { return sensorId; }
private:
int width = NOT_SET_NUM;
int height = NOT_SET_NUM;
double timestamp = 0.0;
int sensorId = -1;
};
Step 2: Add the Nested Metadata Struct for Registration
To enable the declarative pipeline system to recognize your type, add a public nested struct named Metadata with static constexpr members. This struct supplies compile-time constants used by the REGISTER_FRAME_TYPE macro defined in base/include/declarative/FrameTypeRegistry.h.
Add this inside your class definition:
struct Metadata {
static constexpr std::string_view name = "SensorMetadata";
static constexpr std::string_view parent = "FRAME"; // Inherits from generic FRAME
static constexpr std::string_view description = "Metadata for sensor-derived frames with GPS and timestamp";
static constexpr std::array<std::string_view, 2> tags = { "sensor", "enriched" };
};
The parent field establishes inheritance in the type hierarchy. Use "FRAME" for top-level custom types, or reference another registered type like "RawImageMetadata" if your metadata extends a specific built-in class.
Step 3: Register the Type with REGISTER_FRAME_TYPE
Place the registration macro in a source file (.cpp) that is always compiled with your application. This creates a static initialization object that adds your type to the global apra::FrameTypeRegistry at program startup.
// SensorMetadata.cpp
#include "SensorMetadata.h"
REGISTER_FRAME_TYPE(SensorMetadata);
Do not place this macro in a header file, as it would create multiple registration entries if the header is included in multiple translation units.
Using Custom FrameMetadata in Pipeline Modules
Attaching Metadata to Output Pins
When a module declares an output pin, it passes a metadata instance that describes the frames that pin will produce. Downstream modules use this metadata to validate compatibility and allocate resources.
class SensorProducer : public Module {
public:
SensorProducer() {
// Create metadata instance
auto meta = framemetadata_sp(
new SensorMetadata(1920, 1080, 0.0, 42)
);
// Register output pin with this metadata
addOutputPin(meta);
}
bool produce(frame_sp& out) override {
// Update timestamp dynamically
auto meta = FrameMetadataFactory::downcast<SensorMetadata>(
getOutputMetadata(0)
);
meta->timestamp = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
// Create frame and attach metadata
out = makeFrame(meta->getDataSize(), "output0");
out->setMetadata(meta);
return true;
}
};
Safely Retrieving and Downcasting Metadata
Downstream modules receive generic frame_sp objects. To access your custom fields, retrieve the metadata pointer and downcast it using the type-safe helper in base/include/FrameMetadataFactory.h.
class SensorConsumer : public Module {
public:
SensorConsumer() {
// Declare expected input type
addInputPin(framemetadata_sp(new SensorMetadata()));
}
bool process(frame_container& input, frame_sp& out) override {
// Retrieve frame by type name
auto frame = Module::getFrameByType(
input,
SensorMetadata::Metadata::name
);
// Safe downcast to custom type
auto meta = FrameMetadataFactory::downcast<SensorMetadata>(
frame->getMetadata()
);
// Access enriched data
std::cout << "Sensor " << meta->getSensorId()
<< " @ " << meta->getTimestamp() << "s\n";
// Pass through
out = frame;
return true;
}
};
The FrameMetadataFactory::downcast<T>() function provides runtime type checking, ensuring you cannot accidentally interpret metadata as the wrong type.
Complete Working Example: SensorMetadata Implementation
Below is a minimal, self-contained implementation demonstrating the full lifecycle of a custom metadata type—from class definition through registration to production and consumption in pipeline modules.
// ------------------- SensorMetadata.h -------------------
#pragma once
#include "FrameMetadata.h"
#include "FrameMetadataFactory.h"
class SensorMetadata : public FrameMetadata {
public:
// Constructors
SensorMetadata() : FrameMetadata(FrameType::GENERAL) {}
SensorMetadata(int w, int h, double ts, int id)
: FrameMetadata(FrameType::GENERAL), width(w), height(h),
timestamp(ts), sensorId(id) {}
// Required overrides
void reset() override {
FrameMetadata::reset();
width = NOT_SET_NUM;
height = NOT_SET_NUM;
timestamp = 0.0;
sensorId = -1;
}
bool isSet() override { return width != NOT_SET_NUM; }
size_t getDataSize() override {
return sizeof(width) + sizeof(height) +
sizeof(timestamp) + sizeof(sensorId);
}
// Accessors
int getWidth() const { return width; }
int getHeight() const { return height; }
double getTimestamp() const { return timestamp; }
int getSensorId() const { return sensorId; }
// Registration metadata
struct Metadata {
static constexpr std::string_view name = "SensorMetadata";
static constexpr std::string_view parent = "FRAME";
static constexpr std::string_view description = "Metadata for sensor-derived frames";
static constexpr std::array<std::string_view, 2> tags = { "sensor", "custom" };
};
private:
int width = NOT_SET_NUM;
int height = NOT_SET_NUM;
double timestamp = 0.0;
int sensorId = -1;
};
// ------------------- SensorMetadata.cpp -------------------
#include "SensorMetadata.h"
REGISTER_FRAME_TYPE(SensorMetadata);
// ------------------- ProducerModule.cpp -------------------
#include "SensorMetadata.h"
#include "Module.h"
class SensorProducer : public Module {
public:
SensorProducer() {
auto meta = framemetadata_sp(
new SensorMetadata(1920, 1080, 0.0, 42)
);
addOutputPin(meta);
}
bool produce(frame_sp& out) override {
auto meta = FrameMetadataFactory::downcast<SensorMetadata>(getOutputMetadata(0));
meta->timestamp = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()
).count();
out = makeFrame(meta->getDataSize(), "output0");
out->setMetadata(meta);
return true;
}
};
// ------------------- ConsumerModule.cpp -------------------
#include "SensorMetadata.h"
#include "Module.h"
#include "FrameMetadataFactory.h"
class SensorConsumer : public Module {
public:
SensorConsumer() {
addInputPin(framemetadata_sp(new SensorMetadata()));
}
bool process(frame_container& input, frame_sp& out) override {
auto frame = Module::getFrameByType(input, SensorMetadata::Metadata::name);
auto meta = FrameMetadataFactory::downcast<SensorMetadata>(frame->getMetadata());
std::cout << "Received frame from sensor " << meta->getSensorId()
<< " at " << meta->getTimestamp() << "s\n";
out = frame;
return true;
}
};
Summary
Creating custom FrameMetadata types in ApraPipes requires three essential steps:
- Inherit from
FrameMetadataand implement the pure virtual methodsreset()andisSet()to manage initialization state, plusgetDataSize()if your metadata carries binary payload information. - Embed a nested
Metadatastruct containing static constexpr fields (name,parent,description,tags) that theREGISTER_FRAME_TYPEmacro consumes to populate the globalFrameTypeRegistry. - Use
FrameMetadataFactory::downcast<T>()when retrieving metadata in downstream modules to safely convert the base pointer back to your specific type without risking undefined behavior.
Once registered via REGISTER_FRAME_TYPE, your custom metadata integrates seamlessly with the declarative JSON pipeline system, automatic documentation generation, and the existing module ecosystem.
Frequently Asked Questions
How do I ensure my custom FrameMetadata type is thread-safe?
The FrameMetadata base class itself does not enforce thread-safety; it stores only lightweight descriptors. When you add custom fields (e.g., timestamps, sensor IDs), treat metadata instances as immutable after they are attached to a frame, or protect mutable state with your own synchronization primitives if modules will modify metadata concurrently. The recommended pattern is to create a new metadata instance per frame rather than mutating shared metadata objects.
Can I inherit from existing metadata types like RawImageMetadata instead of FrameMetadata?
Yes. If your enriched data extends a specific built-in type (for example, adding GPS coordinates to image metadata), inherit from RawImageMetadata instead of the base FrameMetadata class. Update the nested Metadata struct’s parent field to "RawImageMetadata" so the registry reflects the inheritance hierarchy. This allows downstream modules to treat your type as both your specific class and as a raw image metadata where appropriate.
Where should I place the REGISTER_FRAME_TYPE macro to avoid linker errors?
Place the REGISTER_FRAME_TYPE(MyClass) macro in exactly one source file (.cpp), not in a header. The macro expands to a static object definition that executes registration code during program initialization. If placed in a header included by multiple translation units, you will encounter multiple definition linker errors. A dedicated MyCustomMetadata.cpp file that includes the header and contains only the registration line is the standard pattern.
How do I access custom metadata in Python bindings if I extend the framework?
The ApraPipes Python bindings expose metadata through the same FrameMetadata base interface. To access custom fields from Python, you must expose your specific metadata class using pybind11 (or the binding generator used by the project), providing property getters for your custom fields. Alternatively, serialize your enriched data into the frame's raw buffer and parse it on the Python side if you cannot extend the bindings. The C++ FrameMetadataFactory::downcast approach remains the most efficient method for C++ modules.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →