FrameContainerQueue Architecture for Frame Buffering in ApraPipes
The FrameContainerQueue implements a three-layer buffering architecture—bounded buffer core, domain-specific wrapper, and adapter/strategy layer—that enables thread-safe, high-throughput frame movement between pipeline modules.
The FrameContainerQueue is the central buffering component in the apra-labs/aprapipes repository, responsible for managing how video frames flow between processing stages. Understanding its architecture is essential for building reliable real-time video pipelines that handle back-pressure and concurrent access correctly.
Three-Layer Architecture Overview
The FrameContainerQueue for frame buffering is built on three distinct layers that separate synchronization primitives from domain logic:
| Layer | Role | Implementation |
|---|---|---|
| Bounded Buffer | Low-level, thread-safe circular buffer storing raw frame_container objects with capacity limits and synchronization. |
bounded_buffer template in base/include/BoundBuffer.h |
| FrameContainerQueue | Thin wrapper inheriting from bounded_buffer<frame_container> that exposes a video-pipeline API (push, pop, try_push, etc.). |
base/include/FrameContainerQueue.h and base/src/FrameContainerQueue.cpp |
| Queue Adapter & Push Strategies | Flexible integration layer allowing different queue implementations and policies for when frames should be enqueued (always, try-push, or never). | FrameContainerQueueAdapter in base/include/FrameContainerQueue.h and QuePushStrategy hierarchy in base/include/QuePushStrategy.h |
Layer 1: Bounded Buffer Core
The foundation of the FrameContainerQueue is the bounded_buffer template defined in base/include/BoundBuffer.h. This class provides the thread-safe circular buffer that handles synchronization, capacity limits, and the accept flag used during pipeline start-up.
Capacity and Back-Pressure
The buffer is constructed with a fixed capacity. The push method blocks until space becomes available, providing natural back-pressure, while try_push returns false immediately when the buffer is full.
Dual-Direction Pushes
The buffer supports inserting elements at both ends:
pushinserts at the front (used for high-priority command frames)push_backappends at the back (standard data flow)push_drop_oldestmaintains constant buffer size by discarding the oldest element when full
Synchronization Primitives
A std::mutex and two std::condition_variable instances (m_not_empty, m_not_full) guarantee safe concurrent access between producers and consumers. The is_ready_to_accept predicate also respects the accept flag that is cleared during clear() and set by accept().
Public API
The bounded buffer exposes pop, peek, clear, flush, size, and isFull methods that preserve internal locking while providing standard queue operations.
Layer 2: FrameContainerQueue Wrapper
FrameContainerQueue in base/include/FrameContainerQueue.h is a thin wrapper that inherits from bounded_buffer<frame_container>. It does not add extra logic; instead, it provides the rest of the codebase with a type-safe name that conveys the intent of "frame buffering."
Construction and Delegation
The queue is constructed with a desired capacity (FrameContainerQueue(size_t capacity)). All methods—including push, push_back, push_drop_oldest, pop, try_push, try_pop, peek, isFull, clear, and flush—delegate directly to the base class, ensuring identical semantics to the bounded buffer.
Public Inheritance
Because the class inherits publicly from bounded_buffer<frame_container>, it can be used wherever the generic bounded buffer is expected, while the explicit FrameContainerQueue name improves code readability and maintainability.
Implementation details are located in base/src/FrameContainerQueue.cpp (lines 4–65).
Layer 3: Adapter and Push Strategies
The final layer provides flexible integration patterns that allow modules to plug in different queue implementations or mock objects, and to decide when a frame should be enqueued.
FrameContainerQueueAdapter
The FrameContainerQueueAdapter class (defined in base/include/FrameContainerQueue.h, lines 28–55) holds a boost::shared_ptr<FrameContainerQueue> mAdaptee.
It overrides queue operations such as push, pop, try_push, try_pop, and isFull to forward calls to the underlying queue after evaluating a decision made by should_push.
The PushType enum (DONT_PUSH, TRY_PUSH, MUST_PUSH) allows derived adapters to customize when a frame should be enqueued—never, only if space is available, or always (blocking).
Hook methods (on_failed_push, on_push_success, on_failed_pop, on_pop_success) enable modules to react to push/pop outcomes without cluttering the core queue logic.
QuePushStrategy
For scenarios requiring multi-destination pushes (such as broadcasting a frame to several downstream modules), the QuePushStrategy hierarchy in base/include/QuePushStrategy.h provides higher-level policies.
Strategies include:
- BLOCKING: Waits until space is available in the destination.
- NON_BLOCKING_ANY: Pushes to the first queue that can accept the frame.
- NON_BLOCKING_ALL_OR_NONE: Pushes only if all destinations can accept the frame; otherwise, drops.
The strategy maintains a map mQueByModule of destination IDs to FrameContainerQueue pointers, and its push method iterates according to the selected policy.
Data Flow Through the Pipeline
Understanding how the FrameContainerQueue for frame buffering operates within the full pipeline context clarifies its role in the ApraPipes architecture.
-
Module Creation – Each
Moduleconstructs its ownFrameContainerQueuewith a capacity defined in the module’s properties (_props.qlen). -
Adapter Attachment – Modules requiring custom push behavior (such as demuxers or transforms) create a
FrameContainerQueueAdapter, calladapt(otherQueue), and overrideshould_pushif needed. -
Producer Stage – A module pushes frames into its queue via
queue->push(frame)(blocking) orqueue->try_push(frame)(non-blocking). -
Consumer Stage – Downstream modules call
queue->pop()(blocking) orqueue->try_pop()(non-blocking) to retrieve frames. -
Broadcasting – When a module broadcasts a frame to multiple downstream queues, it uses a
QuePushStrategyinstance that orchestrates the pushes according to the selected policy (blocking, any, or all-or-none).
This architecture guarantees thread-safe buffering with deterministic back-pressure, extensibility via adapters and push-strategies without modifying core queue code, and clear separation between low-level synchronization (bounded_buffer) and high-level pipeline semantics.
Code Examples
The following examples demonstrate practical usage patterns for the FrameContainerQueue architecture.
// 1️⃣ Create a queue with capacity for 30 frames
auto q = boost::make_shared<FrameContainerQueue>(30);
// 2️⃣ Simple push / pop (blocking)
frame_container f = readFrame(); // obtain a frame from some source
q->push(f); // blocks if the queue is full
frame_container out = q->pop(); // blocks until a frame is available
// 3️⃣ Non-blocking push – useful in a real-time source
if (!q->try_push(f)) {
LOG_WARN << "Dropping frame because downstream is saturated\n";
}
// 4️⃣ Using an adapter to conditionally drop frames
class MyAdapter : public FrameContainerQueueAdapter {
protected:
PushType should_push(frame_container) override {
// e.g., drop every 10th frame
static int cnt = 0;
return (++cnt % 10 == 0) ? DONT_PUSH : MUST_PUSH;
}
};
auto adapter = boost::make_shared<MyAdapter>();
adapter->adapt(q); // forward all calls to `q`
// 5️⃣ Broadcasting with a non-blocking "any" strategy
boost::shared_ptr<QuePushStrategy> strat =
QuePushStrategy::getStrategy(QuePushStrategy::NON_BLOCKING_ANY, srcId);
strat->addQue(dstId1, q1);
strat->addQue(dstId2, q2);
strat->push(dstId1, f); // pushes to the first queue that can accept
Key Source Files
| File | Description | Link |
|---|---|---|
base/include/BoundBuffer.h |
Generic thread-safe bounded buffer used by the queue. | BoundBuffer.h |
base/include/FrameContainerQueue.h |
Declaration of FrameContainerQueue and its adapter. |
FrameContainerQueue.h |
base/src/FrameContainerQueue.cpp |
Implementation of the wrapper methods. | FrameContainerQueue.cpp |
base/include/QuePushStrategy.h |
Push-strategy infrastructure for broadcasting frames. | QuePushStrategy.h |
base/src/Module.cpp (relevant parts) |
Shows how each module owns a FrameContainerQueue. |
Module.cpp |
Summary
- The FrameContainerQueue relies on a three-layer architecture: a generic
bounded_bufferfor thread-safe storage, a thinFrameContainerQueuewrapper for type safety, and an adapter/strategy layer for flexible integration. - Thread safety is guaranteed by
std::mutexandstd::condition_variableinbase/include/BoundBuffer.h, supporting both blocking and non-blocking operations. - Back-pressure handling occurs naturally through blocking
pushcalls, whiletry_pushandpush_drop_oldestoffer alternatives for real-time scenarios. - Extensibility comes from
FrameContainerQueueAdapterandQuePushStrategy, allowing custom push logic and multi-destination broadcasting without modifying core queue code. - Key files are located in
base/include/BoundBuffer.h,base/include/FrameContainerQueue.h, andbase/include/QuePushStrategy.h.
Frequently Asked Questions
What is the difference between FrameContainerQueue and bounded_buffer?
bounded_buffer is a generic C++ template class that provides thread-safe circular buffer functionality for any data type, while FrameContainerQueue is a specific instantiation and thin wrapper that inherits from bounded_buffer<frame_container>. The wrapper provides a domain-specific name that clarifies intent within the ApraPipes video processing pipeline, though it delegates all operations directly to the base class.
How does FrameContainerQueue handle back-pressure in real-time pipelines?
The queue handles back-pressure through its blocking push method, which waits on a std::condition_variable until space becomes available when the buffer reaches capacity. For real-time scenarios where blocking is unacceptable, the queue offers try_push for immediate failure feedback, and push_drop_oldest to maintain constant buffer size by discarding stale frames when new ones arrive.
What is the purpose of FrameContainerQueueAdapter?
FrameContainerQueueAdapter provides a flexible integration layer that allows pipeline modules to customize enqueue behavior without modifying the core queue implementation. It holds a reference to a FrameContainerQueue and overrides push operations to evaluate a should_push predicate, enabling conditional frame dropping, and provides hooks like on_failed_push and on_push_success for modules to react to queue state changes.
How does QuePushStrategy support multi-destination broadcasting?
QuePushStrategy implements higher-level policies for pushing frames to multiple downstream queues simultaneously, managing the complexity of partial failures. It supports strategies such as BLOCKING (wait for all destinations), NON_BLOCKING_ANY (push to first available queue), and NON_BLOCKING_ALL_OR_NONE (atomic push to all or drop), allowing modules to optimize for latency versus reliability when broadcasting frames.
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 →