Haiku BMediaEventLooper: Timed Events in Media Nodes
Implement Haiku media-node control with BMediaEventLooper, performance-time queues, event cleanup, late-event policy, and safe thread startup and shutdown.
BMediaEventLooper is a base class for Haiku Media Kit nodes that need a control thread and timed-event queues. It receives node messages, places work onto event queues, and calls a subclass’s HandleEvent() when an event is due. It is a scheduling framework, not a guarantee that arbitrary application code will meet a real-time deadline. A node still has to bound its work, report latency, manage event ownership, and implement its graph lifecycle.
The core design benefit is that media timing and control messages share a disciplined node loop. Instead of creating a private polling thread and sleeping until each event, a node can express work in performance time and let the event machinery dispatch it. This keeps event ordering visible and gives the Media Kit a place to account for run mode, lateness, and time-source changes.
Lifecycle and control-thread ownership
BMediaEventLooper derives from BMediaNode and exposes lifecycle hooks such as NodeRegistered(), Start(), Stop(), Seek(), TimeWarp(), and SetRunMode(). The public header says Run() spawns and resumes the control thread and must be called from NodeRegistered(). It also says Quit() must be called from the destructor. Respect those boundaries: do not start the loop in a constructor before registration or forget to stop it during destruction.
The looper’s control thread waits for messages, dispatches events, and calls HandleEvent(). The header explicitly says subclasses must override HandleEvent() and should not call it directly. Direct invocation bypasses queue timing and can produce reentrancy that the event-loop contract does not expect. To cause work at a time, add a timed event through the appropriate queue API; to receive an immediate control action, implement the relevant node hook or message behavior.
Keep constructors inexpensive. Establish state, validate configuration, and let the registration lifecycle start the control loop. If node registration fails, ensure the partially constructed node does not leave a thread running. During teardown, stop accepting new work, cancel or clean pending custom events, and call Quit() as required. Do not destroy state used by HandleEvent() before the loop thread has terminated.
Events use performance time, not arbitrary wall-clock sleeps
Timed events contain an event time and type plus optional data. The event queues order work against the node’s media timing contract. Construct timestamps in the selected time source’s performance-time domain. A Unix timestamp or a value from an unrelated monotonic clock is not automatically compatible. Convert only through the time-source APIs and account for time warps.
The class provides EventQueue() and RealTimeQueue() accessors. Choose the queue according to the event’s semantics and the API contract, not because one sounds universally faster. Normal media events are associated with the performance timeline; real-time queue work has different scheduling implications. Consult the header and queue implementation for the exact event types used by the target Haiku revision.
An event can be late by the time it reaches HandleEvent(). The method receives a lateness value and a real-time-event flag. Use these inputs to apply a deliberate policy. A late audio control change may still need to be applied immediately; an obsolete video frame might be dropped; a state transition may need to update the model but avoid replaying stale output. Do not silently pretend that a late event happened on time.
Keep HandleEvent bounded
The event loop is not the place for unbounded disk scans, blocking network requests, user prompts, or long computations. A slow handler delays later events and can increase graph latency. Parse and validate event data quickly, update bounded in-memory state, and hand expensive work to an appropriate worker when the node design permits. If handing off, preserve ordering and completion semantics explicitly; a worker thread can otherwise reorder operations after the event loop has serialized them.
Do not block the handler waiting for a consumer to recycle buffers without a timeout strategy. Avoid acquiring locks that may be held by a UI thread waiting for media work. Keep lock ordering documented. A deadlock in the control thread can look like a media-server failure even when the server is responsive.
The looper exposes SetEventLatency(), SetBufferDuration(), SetPriority(), SchedulingLatency(), and EventLatency()-related state. Report values that reflect the node’s actual processing behavior. Do not set priority to the maximum as a generic fix: competition for CPU can worsen system responsiveness. The header notes the priority is clamped to a range, which is not a reason to request an extreme value without measurement.
Event cleanup and ownership
Custom event payloads need an explicit lifetime. CleanUpEvent() exists so subclasses can properly clean up custom events removed from or flushed out of the queue. Define who owns any pointer or allocated data attached to an event. A queued event may be canceled during stop, seek, time warp, or teardown, and the normal HandleEvent() path may never run. If cleanup is only performed after handling, cancellation leaks resources.
Use event type and payload validation before interpreting attached data. Do not cast an integer field into a pointer without a documented representation. Avoid storing a pointer to a stack object in an event that can execute after the originating method returns. If the event carries a reference-counted object, make the retain/release rules explicit and test every cancellation path.
When removing or flushing events, ensure the cleanup callback is compatible with the operation and does not re-enter the queue in an unsupported way. Follow queue APIs rather than manipulating internal list structures. Check returned status codes and log failed event operations in a rate-limited way.
Start, stop, seek, and time warp behavior
Lifecycle methods should update the node’s state and its pending events coherently. A Stop() can be immediate or occur at a requested performance time; the immediate parameter matters. Seek() may invalidate future events based on the previous position. TimeWarp() changes how performance and real time relate. A node that updates only its playback cursor but leaves stale events queued can emit data from the old timeline after a seek.
Define what happens to each event class during these transitions. Some state-setting events may be safe to replay; one-shot notifications may not be. Cancel obsolete work, clean its payload, and schedule any new work relative to the updated performance time. Repeated start/stop and seek sequences should not accumulate duplicate events.
Offline mode needs separate consideration. The class has OfflineTime() for offline operation and the header describes it as a hook for determining the node’s current time. Do not assume a real-time sleep-based strategy is appropriate in offline processing. Update the node’s internal time when it changes and keep ordering deterministic, while still respecting the Media Kit’s offline scheduling contract.
A small subclass shape
The core subclass contract can be summarized without inventing a complete node implementation:
class MeterNode : public BMediaEventLooper {
protected:
void NodeRegistered() override
{
BMediaEventLooper::NodeRegistered();
Run();
}
void HandleEvent(const media_timed_event* event,
bigtime_t lateness, bool realTimeEvent) override
{
// Validate event type and payload, then do bounded work.
}
void CleanUpEvent(const media_timed_event* event) override
{
// Release only payloads owned by this subclass.
}
};
This sketch omits constructor/destructor details and other required BMediaNode or producer/consumer hooks. Confirm virtual signatures against the installed Haiku headers. A base call in NodeRegistered() and Run() must follow the complete node’s lifecycle requirements, including error handling and ensuring Quit() is called from destruction.
Observability and test strategy
Instrument event type, requested performance time, actual dispatch time, lateness, selected queue, node run mode, and whether the event was handled or canceled. Track the distribution of lateness instead of reporting only an average; a small mean can hide rare deadline misses. Do not log every audio-period event synchronously to disk. Use counters, sampling, and bounded ring buffers for diagnostics.
Test events scheduled in the future, already-late events, equal-time events, cancellation, queue flush, seek, time warp, and offline operation. Include malformed payloads and allocation failures. Simulate slow downstream processing and lock contention. Verify no event is handled twice and every owned payload is released exactly once whether it runs or is canceled. Repeat start/stop and destruction while work is pending.
For a production node, define acceptance thresholds before testing: maximum acceptable lateness for each event class, buffer starvation behavior, shutdown deadline, and memory bound on queued event payloads. Tie every threshold to a user-visible or graph-level requirement. The queue makes timed work explicit, but only measurements can establish whether the node meets its deadlines.
BMediaEventLooper is most useful when a node’s state changes need to be coordinated with media time. Use its registration-owned thread and queues, keep handlers bounded, treat cancellation as a first-class path, and make timing transitions explicit. That avoids the false simplicity of a private sleep loop and produces a node whose scheduling behavior can be inspected and tested.
Related:
- Haiku BMessageRunner: Periodic Messages Without a Polling Thread
- The Media Kit: Real-Time Audio and Video in Haiku
Sources: