Haiku BMediaRecorder: Capture a Media Source with Bounded Callbacks
Use Haiku BMediaRecorder to connect a media source, negotiate accepted formats, process timestamped buffers, and stop or disconnect cleanly.
BMediaRecorder is a convenience node for receiving media buffers from a Media Kit source and handing them to application callbacks. It can simplify a small recorder or monitor, but it does not write a file format, guarantee a hardware input is working, or remove the need to handle the Media Kit’s node, format, timing, and ownership rules. Think of it as a graph consumer with lifecycle helpers, not a full recording application.
Check initialization before connecting
Construction registers an internal recorder node with the Media Kit. Always call InitCheck() before using the object. If Media Server initialization or node registration fails, preserve that status and show a useful error; calling Connect() anyway cannot repair a failed registration.
The Connect() overloads have different intents. Connect(format) asks the recorder to use a default source for selected media types; the current implementation chooses the audio mixer for raw audio and the video input for raw or encoded video. It rejects other format types in that overload. Connect(dormant_node_info, format) instantiates a dormant node, while Connect(media_node, output, format) lets the application specify an existing node and optionally an output. Do not infer that every audio/video type can be connected through every overload.
Set the accepted format before connection when the capture path needs constraints. AcceptedFormat() exposes that policy. For reliable behavior, request a format the producer can provide, inspect the final Format() after connecting, and display the actual media properties. A requested format is not a promise: the source and recorder node negotiate the compatible connection.
Install callbacks with explicit lifetime
SetHooks() installs a ProcessFunc, a NotifyFunc, and an opaque cookie. The process callback receives the cookie, a timestamp, a data pointer, byte length, and media_format. Treat the data as callback-scoped unless the API explicitly documents otherwise. If the application needs to encode or write asynchronously, copy the payload into an application-owned bounded queue before returning.
void Process(void* cookie, bigtime_t timestamp, void* data,
size_t size, const media_format& format)
{
RecorderState* state = static_cast<RecorderState*>(cookie);
if (state == NULL || data == NULL)
return;
// This must copy the bytes and metadata into bounded owned storage.
if (!state->queue.TryCopy(data, size, timestamp, format))
state->droppedBuffers++;
}
recorder.SetHooks(Process, Notify, &state);
This is a design sketch, not a complete thread-safe queue. It deliberately does not write to a file or allocate an unbounded object in the callback. Use a synchronization strategy suited to the callback context, and coordinate the cookie’s lifetime so it remains valid until the recorder has stopped and no callback can still use it.
The callback executes as part of the recorder’s incoming-buffer path in current source. Consequently, work duration affects graph throughput. Do not block waiting for a disk flush, a network peer, a UI confirmation, or a mutex held by another thread that might be waiting for recording to stop. If the writer falls behind, apply an explicit policy: report recording failure, pause, or drop according to the product requirement. Quietly accumulating buffers only delays an eventual memory or latency failure.
Treat notification callbacks as state transitions
The NotifyFunc reports recorder-related start, stop, seek, and time-warp notifications using a notification code and variadic arguments. The header lists the meaning of the arguments for each event. Decode those arguments by event type; a performance_time for B_WILL_START is not the same tuple as the values passed for B_WILL_SEEK or B_WILL_TIMEWARP.
Do not store or forward a raw va_list as if it were a stable event object. Copy only documented values into a typed application message, and include the node’s current generation so a late notification from an earlier connection cannot change the new session’s UI. Keep notification processing brief and avoid assuming that a notification means all data buffers before or after the boundary have already been committed to storage.
Connect, start, stop, disconnect
Connection and execution are separate. Start() returns B_MEDIA_NOT_CONNECTED when no graph connection exists. It starts a relevant time source or node and enables the recorder’s data path; IsRunning() reports the helper’s state, not the health of a file writer in your callback. Stop() disables data delivery and requests the node stop. Both calls return statuses that must be checked.
The optional force argument affects how already-running or already-stopped cases are handled in current source; it is not a general “ignore errors” switch. Avoid using it to conceal a lifecycle race. Keep your own states such as initialized, connected, running, stopping, and disconnected; transition only when the API call succeeds. If a stop fails, do not destroy the cookie or the writer thread while callbacks may still be possible.
Disconnect() stops first when the recorder is running, then removes the connection and releases an acquired output-node reference as appropriate. A destructor also calls stop and disconnect for a registered recorder node, but deterministic applications should perform and check teardown explicitly before destruction so they can report failures and flush their own output writer.
One safe lifecycle is: construct; check InitCheck(); set accepted format and callbacks; connect; inspect Format(); start; receive and queue; request stop; wait for the application-owned writer to drain; disconnect; release the callback cookie and writer resources; destroy the recorder. Adapt the sequence if your format requires a particular source or if the application supports reconnects.
Make reconnect a new generation rather than reusing an old queue blindly. Tag each queued item with the recorder generation, prevent a stale callback from entering a replacement session, and drain or explicitly discard old-generation data before opening the next output. If the application offers pause/resume, define whether pause preserves timestamps, inserts a gap, or creates a new segment. Those are recording-format choices, not behaviors the convenience class can choose for you.
Timestamps and output files are your responsibility
The process callback’s timestamp is media timing information. Preserve it if writing a container or synchronizing audio/video. Do not replace it with the wall-clock time that the callback ran. For long recordings, monitor monotonicity, gaps, and drift; document how your file format represents time and how discontinuities are handled.
BMediaRecorder does not choose a container, write headers, flush a file, finalize indexes, or atomically publish a completed recording. Those tasks belong to the application or a separate encoder/muxer path. Do not label a recording complete when the graph stopped but the file writer still has queued data. Write to a temporary output, finalize and check errors, then publish the result only after successful close if that is appropriate for your product.
If capture targets a physical microphone or camera, graph connection alone does not prove the selected device is the one the user expects. Show the source name where possible, verify buffers arrive, and test silent or blank input separately. A recorder with no callback activity may reflect source selection, permissions/state, or a stalled node; collect graph IDs and statuses before blaming storage.
Failure-oriented verification
Test failed initialization, unsupported media type, no matching device, already-connected state, format mismatch, connection loss, Start() before connect, repeated start/stop, slow writer, disk-full or write failure in the worker, disconnect during queued work, and shutdown while a callback is active. Confirm that the callback cookie outlives the final callback and that the application does not free buffers or state early.
Log InitCheck(), requested and accepted formats, connection status, IsConnected(), start/stop status, buffer count, byte count, timestamp range, queue depth, and writer errors. A useful diagnostics view distinguishes “source connected,” “buffers arriving,” “writer keeping up,” and “file finalized.” Those are four different claims.
BMediaRecorder handles useful graph plumbing, but the application still owns the recording contract: choose a source and compatible format, keep callbacks bounded, retain correct timestamps, drain and finalize storage, and unwind the graph explicitly.
Related:
- Haiku BMediaFile and BMediaTrack: Container and Track Workflows
- Haiku BBufferGroup: Media Buffer Pools and Ownership
Sources: