# How to Implement a Custom Source Module for Capturing or Generating Frames in ApraPipes

> Learn to implement a custom source module in ApraPipes to capture or generate frames. Follow our guide to subclass Module, override init and produce, and register your custom module.

- Repository: [Apra Labs/aprapipes](https://github.com/apra-labs/aprapipes)
- Tags: how-to-guide
- Published: 2026-02-25

---

**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`](https://github.com/apra-labs/aprapipes/blob/main/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 `applyProperties` template 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)](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.

```cpp
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)](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()`.

```cpp
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)](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.

```cpp
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)](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.

```cpp
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)](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)](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.

```cpp
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.

1. **Compile the project:**

   ```bash
   cmake -B build -S . && cmake --build build -j$(nproc)
   ```

2. **Verify registration:**

   ```bash
   ./build/aprapipes_cli list-modules --category Source | grep MyCamSource
   ```

3. **Create a test pipeline** ([`mycam_pipeline.json`](https://github.com/apra-labs/aprapipes/blob/main/mycam_pipeline.json)):

   ```json
   {
     "pipeline": [
       {
         "type": "MyCamSource",
         "props": { "width": 1280, "height": 720, "device": 0 }
       },
       {
         "type": "FileWriterModule",
         "props": { "outFolder": "output", "fileNamePrefix": "frame", "format": "yuv" }
       }
     ]
   }
   ```

4. **Execute:**

   ```bash
   ./build/aprapipes_cli run mycam_pipeline.json
   ```

   Frames 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 `ModuleProps` to declare configurable fields.
- **Derive from `Module`** with the `SOURCE` kind to indicate the module produces data.
- **Override `init()`** to create output metadata and register pins with `addOutputPin()`.
- **Implement `produce()`** to allocate frames with `makeFrame()`, fill buffers, and push downstream via `send()`.
- **Override `stop()`** to release hardware resources like cameras or sockets.
- **Register the module** in [`ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/ModuleRegistrations.cpp) with 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.