How to Implement a Custom Source Module for Capturing or Generating Frames in ApraPipes
To implement a custom source module in ApraPipes, create a Props class derived from ModuleProps, subclass Module with the SOURCE kind, override init() to declare output metadata, implement produce() to generate or capture frames using makeFrame() and send(), and register the module in ModuleRegistrations.cpp.
ApraPipes is a high-performance C++ framework for building video processing pipelines. When you need to ingest frames from a proprietary camera, a network stream, or a custom algorithm, you must implement a custom source module for capturing or generating frames that integrates with the framework's declarative pipeline system.
Step 1: Define the Props Class for Configuration
Every source module requires a properties class that inherits from ModuleProps. This class exposes configurable parameters—such as resolution, device ID, or frame rate—to the declarative pipeline system.
Key implementation details:
- Declare public member variables for each setting (e.g.,
int width,int height). - Implement the
applyPropertiestemplate method to bind these fields to JSON configuration keys. - Use
apra::applyProp()to handle type-safe assignment and validation.
Reference implementation: See TestSignalGeneratorProps in [base/include/TestSignalGeneratorSrc.h](https://github.com/apra-labs/aprapipes/blob/main/base/include/TestSignalGeneratorSrc.h#L15-L30).
Step 2: Subclass Module and Set the Source Kind
Create your module class by inheriting from Module and specifying the SOURCE kind in the constructor initializer list. This classification tells the pipeline executor that this module produces data rather than transforming or consuming it.
class MyCamSource : public Module {
public:
MyCamSource(MyCamSourceProps _props)
: Module(SOURCE, "MyCamSource", _props), mProps(_props) {}
// ...
};
For a minimal reference, examine ExternalSourceModule in [base/include/ExternalSourceModule.h](https://github.com/apra-labs/aprapipes/blob/main/base/include/ExternalSourceModule.h#L17-L21).
Step 3: Implement init() to Declare Output Metadata
The init() method establishes the contract between your source and downstream modules by declaring the output pin metadata. You must call the base class Module::init() first, then create a metadata object (e.g., RawImagePlanarMetadata) and register it with addOutputPin().
bool init() override {
if (!Module::init()) return false;
// Define YUV420 output format
metadata = framemetadata_sp(
new RawImagePlanarMetadata(mProps.width, mProps.height,
ImageMetadata::ImageType::YUV420,
0, CV_8U));
mOutputPinId = addOutputPin(metadata); // registers the pin
frameSize = mProps.width * mProps.height * 3 / 2; // YUV420 size
// Initialise camera (pseudo-API)
cam.open(mProps.device, mProps.width, mProps.height);
return cam.isOpened();
}
See the full implementation in TestSignalGenerator::init() within [base/src/TestSignalGeneratorSrc.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/src/TestSignalGeneratorSrc.cpp#L227-L238).
Step 4: Implement produce() to Generate or Capture Frames
The produce() method is the core execution loop. It allocates frames, fills them with data, and pushes them downstream. Return true to continue producing, or false to signal end-of-stream.
bool produce() override {
// Allocate frame
frame_sp frame = makeFrame(frameSize, mOutputPinId);
uint8_t* buf = static_cast<uint8_t*>(frame->data());
// Capture or generate data
if (!cam.read(buf)) return false; // Camera read failed
// Attach metadata and send
frame->setMetadata(metadata);
frame_container frames;
frames.insert({mOutputPinId, frame});
return send(frames);
}
Reference the gradient generation logic in TestSignalGenerator::produce() → mDetail->generate(frame) → send(frames) in [base/src/TestSignalGeneratorSrc.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/src/TestSignalGeneratorSrc.cpp#L238-L260).
Step 5: Handle Cleanup with stop()
If your source acquires hardware resources (cameras, file handles, network sockets), override stop() to release them before the base class cleanup executes.
bool stop() override {
cam.close(); // Release hardware
return Module::stop(); // Base class cleanup
}
See ExternalSourceModule::stop() in [base/include/ExternalSourceModule.h](https://github.com/apra-labs/aprapipes/blob/main/base/include/ExternalSourceModule.h#L72-L83).
Step 6: Register the Module for Declarative Pipelines
To expose your source to JSON pipeline definitions, add a registration block in [base/src/declarative/ModuleRegistrations.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/src/declarative/ModuleRegistrations.cpp). This binds C++ types to property schemas that the CLI can parse.
if (!registry.hasModule("MyCamSource")) {
registerModule<MyCamSource, MyCamSourceProps>()
.category(ModuleCategory::Source)
.description("Captures frames from a webcam or IP camera")
.tags("source", "camera", "capture")
.output("output", "RawImagePlanar")
.intProp("width", "Frame width", true, 640, 1, 4096)
.intProp("height", "Frame height", true, 480, 1, 4096)
.intProp("device", "Camera index", false, 0, 0, 10);
}
Reference the existing registration for TestSignalGenerator at lines 68‑78 in the same file.
Step 7: Build and Test Your Source Module
After implementing the class and registration, compile the project and verify integration.
-
Compile the project:
cmake -B build -S . && cmake --build build -j$(nproc) -
Verify registration:
./build/aprapipes_cli list-modules --category Source | grep MyCamSource -
Create a test pipeline (
mycam_pipeline.json):{ "pipeline": [ { "type": "MyCamSource", "props": { "width": 1280, "height": 720, "device": 0 } }, { "type": "FileWriterModule", "props": { "outFolder": "output", "fileNamePrefix": "frame", "format": "yuv" } } ] } -
Execute:
./build/aprapipes_cli run mycam_pipeline.jsonFrames should be written to
output/and the console will show pipeline statistics if health logging is enabled.
Summary
Implementing a custom source module for capturing or generating frames in ApraPipes requires seven key steps:
- Create a Props class inheriting from
ModulePropsto declare configurable fields. - Derive from
Modulewith theSOURCEkind to indicate the module produces data. - Override
init()to create output metadata and register pins withaddOutputPin(). - Implement
produce()to allocate frames withmakeFrame(), fill buffers, and push downstream viasend(). - Override
stop()to release hardware resources like cameras or sockets. - Register the module in
ModuleRegistrations.cppwith property descriptors and output pin types. - Build and verify using the CLI to list modules and execute a test pipeline.
Frequently Asked Questions
What is the difference between a source module and other module types in ApraPipes?
A source module inherits from Module with the SOURCE kind and acts as the origin of data in a pipeline. Unlike transform modules (which modify incoming frames) or sink modules (which consume frames without producing output), a source module has no input pins and uses produce() to generate or capture frames that flow downstream.
How do I handle different pixel formats in my custom source module?
In the init() method, instantiate the appropriate metadata class for your format. For planar YUV, use RawImagePlanarMetadata; for BGR or RGB interleaved images, use RawImageMetadata. Set the ImageType enum (e.g., ImageMetadata::ImageType::YUV420 or ImageMetadata::ImageType::BGR) and ensure your produce() method fills the frame buffer according to that specific layout and stride.
Can I implement a source module that accepts external frames instead of generating them internally?
Yes. Use ExternalSourceModule as a reference implementation. Instead of overriding produce(), you expose a method like produceExternalFrame(frame_sp frame) that external code calls to inject pre-built frames. This pattern is useful when integrating with third-party SDKs that use callback-based frame delivery or when bridging ApraPipes with external memory pools.
How do I debug a custom source module that is not producing frames?
First, verify the module is registered by running aprapipes_cli list-modules. If registered, add logging inside init() to confirm metadata creation succeeds and addOutputPin() returns a valid ID. In produce(), check that makeFrame() returns a valid pointer and that your data-filling logic writes the expected number of bytes. Finally, ensure send() returns true and that downstream modules are connected correctly in your pipeline JSON. Enable verbose logging with --log-level debug to trace frame flow through the pipeline.
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 →