Haiku BMediaNode: Registration, Identity, and Reference Lifetime
Implement Haiku BMediaNode lifecycle safely with reference counting, registration hooks, node identity, performance-time commands, and orderly error reporting.
BMediaNode is the indirect base class for participants in Haiku’s Media Kit graph. It is not usually the only base of a useful node: the public header recommends more specific interfaces such as BBufferProducer, BBufferConsumer, and other node mix-ins, and permits multiple inheritance. This article focuses on the node’s own identity and lifecycle contract, not the roster’s discovery and connection workflow or the producer/consumer buffer contract.
The class exposes a node name, node ID, kind flags, a media_node snapshot, run mode, and associated time source. It also defines the asynchronous media commands Start, Stop, Seek, SetRunMode, TimeWarp, Preroll, and SetTimeSource, plus completion and error reporting hooks. These operations belong to performance-time coordination; they are not ordinary synchronous setters on a UI model.
Respect reference-counted node ownership
The public header says construction initializes the node’s reference count to one and that Release() should be called to destroy it. Its destructor is protected, so direct delete is not the normal lifetime path. This is a different ownership style from ordinary heap objects and is easy to break when a node is owned by both an add-on and an application.
Document which component owns the initial reference and when ownership transfers. For every additional Acquire(), balance it with Release() on every exit path. Do not call Release() merely because a roster operation returned a media_node structure; a structure containing an ID is not automatically the same thing as a reference acquired through a node API. Likewise, do not keep a raw BMediaNode* beyond the lifetime guaranteed by the component that provided it.
The node’s ID() is a graph identity; Node() returns a value structure containing node information. Keep those separate from a pointer to your in-process C++ object. Media nodes can be controlled through Media Server messaging, so clients should use the documented roster and node APIs rather than dereferencing implementation pointers across team boundaries. If a device or add-on disappears, refresh identity and surface the failure instead of treating an old ID as a permanent object handle.
Treat registration as a lifecycle boundary
NodeRegistered() is a protected virtual hook called by the Media Kit after registration. Derived classes should place registration-dependent setup there, not in a constructor that runs before the node has entered the graph. The more specialized BMediaEventLooper documentation adds its own requirements for starting the event loop; those should be followed by that subclass and not generalized to all nodes.
The node’s AddOn() method reports the add-on that instantiated it, or null for an application-internal class, and an internal ID output. Implement this consistently with the factory path. Do not use AddOn() as a user-facing identity or assume it remains meaningful after the add-on’s lifecycle ends. A node can report Kinds() and add kind flags through the intended system paths; these flags help describe the node but are not a substitute for supported input/output enumeration.
Keep registration, graph connection, running, and stopping as separate states in your model. A node can be registered without being connected, connected without being started, or started while failing to deliver expected data. Make state transitions only after the relevant API or callback confirms them. The node’s existence in a roster list is not an end-to-end health check.
Handle commands in performance time
Media Kit command methods accept performance-time values. Start(atPerformanceTime), Stop(atPerformanceTime, immediate), and Seek(toMediaTime, atPerformanceTime) distinguish when the command applies from the media time it targets. Do not substitute a wall-clock timestamp from system_time() without using the node’s time-source mapping. Keep the time source and the command deadline visible in code so reviewers can tell which clock is involved.
The base class documentation notes that these methods do not return operation errors directly; the framework uses an error reporting mechanism. ReportError() can notify roster watchers with a node error code and optional message. RequestCompleted() is the hook for completed or failed requests. A command accepted for scheduling is not necessarily a command completed successfully. Log the request type, node ID, requested performance time, completion status, and any structured error information.
NodeStopped(performanceTime) communicates that the node has handled a stop request, and the header calls out its importance to clients waiting for stop information, especially offline-capable nodes. Do not report stopped before the node has actually quiesced its work and released or flushed any owned media resources. If an immediate stop cannot be honored safely, document the node’s policy and report the resulting state through the supported error/completion mechanism.
Build a status-aware command path
The code handling HandleMessage() receives an integer message code and a byte buffer with a size. Validate the message size before interpreting its payload. Dispatch recognized messages to the relevant virtual hooks and call HandleBadMessage() for unrecognized requests when appropriate. Do not assume every message comes from your own application or contains a current version of a private structure.
status_t RecorderNode::HandleMessage(int32 message, const void* data,
size_t size)
{
if (message == kSetQuality) {
if (data == NULL || size != sizeof(int32))
return B_BAD_DATA;
int32 quality;
memcpy(&quality, data, sizeof(quality));
if (quality < kMinimumQuality || quality > kMaximumQuality)
return B_BAD_VALUE;
return ApplyQualityAtSafeBoundary(quality);
}
return BMediaNode::HandleMessage(message, data, size);
}
This is a method excerpt: the derived class must implement the required AddOn() and any producer/consumer interfaces, and the message code must be part of the node’s actual control protocol. If the base handler does not recognize a message in the target release, handle the bad-message path according to the public API instead of silently swallowing it. Keep callback work bounded and avoid file I/O or UI waits inside media timing paths.
Use GetNodeAttributes() for the node metadata that the interface is designed to expose. The output capacity is explicit; return the number of attributes actually filled and never write beyond the supplied buffer. Attributes are descriptors, not a secure secret channel or arbitrary pointer transport. Use stable names and types, and make client code tolerate attributes being absent on another node implementation.
Select run modes deliberately
The run-mode enum describes different responses to lateness: B_OFFLINE, B_DECREASE_PRECISION, B_INCREASE_LATENCY, B_DROP_DATA, and B_RECORDING. These are policy hints about how the node should adapt when it cannot meet timing, not universal guarantees that every node can implement every strategy equally. Choose a mode based on the product’s correctness needs. Dropping data may be acceptable for live preview and unacceptable for archival recording.
Keep the requested run mode and the node’s measured behavior separate. A request to increase latency does not prove that downstream consumers tolerate it; a request to drop data does not define which buffers may be discarded. Test the actual node graph and report mode transitions to diagnostics. For deterministic recording, validate output integrity independently rather than assuming B_RECORDING has made all buffers timely.
Shut down in reverse lifecycle order
A node should stop accepting new work, handle outstanding performance-time requests, stop or disconnect its data interfaces as required, and then release its reference when its owning component is done. Do not destroy a node object while a callback or request completion can still reach it. Coordinate object reference lifetime with thread shutdown; reference counting keeps the node allocated but does not automatically make every member thread-safe.
In constructors, establish only local state that does not require registration. In the registration hook, finish system-dependent setup and make errors observable. In destructors, stop owned threads and release resources before the final Release() path makes destruction possible. A destructor that calls back into a dead roster or waits on a thread that needs the node’s lock can deadlock the shutdown path.
Verify node lifecycle failure modes
Test application-internal nodes and add-on-created nodes, registration failure, a missing time source, late Start/Stop/Seek commands, repeated Stop, completion failure, add-on removal, a media server restart, and final reference release. Confirm that each acquired reference is balanced and that user-facing UI state does not remain “running” after a node reports failure.
Capture node ID, media_node structure, kind flags, time-source ID, run mode, command performance time, and status in a diagnostic bundle. Keep payloads and timing metadata separate from wall-clock logs. Verify actual data flow through a connected graph instead of using successful registration as a proxy.
BMediaNode provides the identity and control foundation for more specialized media interfaces. Reliable implementations treat registration, graph activity, command completion, and object lifetime as distinct states; schedule against performance time; report errors; and balance every reference through Release() rather than direct deletion.
Related:
- Haiku BMediaRoster: Discovering Nodes and Building Media Graphs
- Haiku BMediaAddOn: Media Node Factories and Flavor Discovery
Sources: