BlueZ, Bluetooth services, and audio routing

Trace Bluetooth service state and audio routing boundaries.

14 min readChapter 142 of 168Intermediate → advanced
In this chapter

Before you start#

Read ALSA userspace audio, PipeWire session policy, and D-Bus messages. BlueZ 5.78 and PipeWire 1.2.0 are separate historical source baselines, not a tested hardware or codec compatibility matrix. The exercise uses invented capabilities and a private pipe; it never contacts Bluetooth hardware or a system bus.

Learning goals#

Separate discovery, pairing, trust and connection; trace an endpoint configuration into a transport and its fd lifetime. Explain differences among media profiles and handle an asynchronous disconnect or failed acquisition without exposing credentials.

Source walkthrough#

Bluetooth media transport lifecycle
Bluetooth media transport lifecycle1. Policy / endpoint to BlueZ: Register endpoint and select configuration. 2. BlueZ to Policy / endpoint (return): Set selected configuration. 3. Policy / endpoint to Media transport: Acquire transport when permitted. 4. Media transport to Policy / endpoint (return): Return fd and MTUs. 5. Policy / endpoint to Media transport: Release or handle disconnect.Policy / endpointBlueZMedia transportRegister endpoint andselect configurationSet selectedconfigurationAcquire transport when permittedReturn fd and MTUsRelease or handle disconnect
  1. Policy / endpointBlueZ

    Register endpoint and select configuration

  2. BlueZPolicy / endpoint

    Return. Set selected configuration

  3. Policy / endpointMedia transport

    Acquire transport when permitted

  4. Media transportPolicy / endpoint

    Return. Return fd and MTUs

  5. Policy / endpointMedia transport

    Release or handle disconnect

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Policy / endpoint → BlueZ: Register endpoint and select configuration
  2. BlueZ → Policy / endpoint (return): Set selected configuration
  3. Policy / endpoint → Media transport: Acquire transport when permitted
  4. Media transport → Policy / endpoint (return): Return fd and MTUs
  5. Policy / endpoint → Media transport: Release or handle disconnect
Conceptual model. A hypothetical authorized lifecycle, not Bluetooth activity.

BlueZ is a Linux Bluetooth userspace stack whose daemon exposes services over the system D-Bus in common deployments. Adapter1 represents a controller-facing adapter, and Device1 represents a remote device known to the daemon. ObjectManager lets authorized clients enumerate objects and receive additions and removals. A D-Bus object path is a handle within a bus service lifetime, not an immutable physical identity or a guarantee that the remote radio device remains reachable. Controller support, daemon configuration, profile implementation, permissions and remote device capabilities all affect what is available.

Discovery finds advertisements or devices under the adapter's configured behavior; it is not pairing. Pairing establishes the relevant security relationship using a protocol and user interaction where required; it is not the same as marking a device trusted. A trust property is a local policy input and does not prove that a connection is currently active. Connection establishes one or more links or profiles under their protocols; it does not mean every supported service is available. A device can disconnect while its D-Bus object persists, and a daemon restart can invalidate proxies even if the physical device remains nearby.

An Agent1 implementation handles authorization and pairing interaction under the daemon's policy. It can be asked to display, request or confirm information depending on the pairing method. Such information can be sensitive. An agent's ability to receive and respond to these requests is an authority boundary, not a general license for unrelated applications to obtain credentials. A cancelled prompt, disappearing agent, user rejection or bus disconnect needs a defined failure route. This chapter neither registers an agent nor reads passkeys, addresses or contacts. See the BlueZ 5.78 agent API and device API.

Media endpoints and configuration

An audio service can register a media endpoint through BlueZ's Media1.RegisterEndpoint interface. The endpoint advertises properties and capabilities appropriate to a profile and codec. BlueZ can invoke endpoint methods to select a configuration from compatible capabilities and then set that configuration for a transport. SelectConfiguration and SetConfiguration are distinct operations: selecting a candidate does not itself grant an acquired transport fd. ClearConfiguration and endpoint removal participate in teardown. The exact method availability and argument definitions depend on the pinned BlueZ interface and negotiated profile.

The BlueZ 5.78 media implementation demonstrates a concrete A2DP configuration path. Its set_configuration function looks for an existing device transport, creates a transport for the selected configuration, then allocates a D-Bus method call to the endpoint. If an earlier stage fails, it returns failure rather than proceeding with an uninitialized transport. The pending D-Bus message and associated callback state must remain valid until the asynchronous result or cancellation path releases them.

From BlueZ 5.78 profiles/audio/media.c, set_configuration, GPL-2.0-or-later SPDX license; earlier declarations and later list handling are omitted:

c
	if (msg == NULL) {
		error("Couldn't allocate D-Bus message");
		media_transport_destroy(transport);
		return FALSE;
	}

The condition detects failure to allocate the endpoint method call. The first statement records the error; the next destroys the transport created earlier in the function; the final statement reports failure. This is a concrete unwind of a partially constructed object, not a general rule that every BlueZ error destroys every transport. If the call was sent and the endpoint later disconnects, the asynchronous callback and owner cleanup must also account for any pending request and references. A path string kept after the endpoint vanishes is not a live service.

Transport state and fd ownership

A MediaTransport1 object represents a configured media transport. Its state, owner and operations are separate from the device's pairing and connection properties. Acquire and TryAcquire request access under different interface conditions; Release relinquishes the acquired transport. A successful acquisition returns a Unix fd and read/write MTU values for that transport. The fd is a reference to a kernel communication object, not the audio samples themselves or a persistent device identity. The MTUs constrain packet handling; they are not the PCM frame size or an end-to-end latency guarantee.

In profiles/audio/transport.c, BlueZ 5.78, GPL-2.0-or-later SPDX (surrounding code omitted):

c
typedef enum {
	TRANSPORT_STATE_IDLE,		/* Not acquired and suspended */
	TRANSPORT_STATE_PENDING,	/* Playing but not acquired */
	/* Playing but not acquired, applicable only for transports
	 * created by a broadcast sink
	 */
	TRANSPORT_STATE_BROADCASTING,
	TRANSPORT_STATE_REQUESTING,	/* Acquire in progress */
	TRANSPORT_STATE_ACTIVE,		/* Acquired and playing */
	TRANSPORT_STATE_SUSPENDING,     /* Release in progress */
} transport_state_t;

The enum order is what makes the acquire check transport->state >= TRANSPORT_STATE_REQUESTING cover the requesting, active and suspending states. The excerpt does not show when each transition happens, and it does not show the broadcast-sink behavior in detail, so it is an ordering reference rather than a complete state machine.

BlueZ transport state order
  1. IDLE

    Not acquired and suspended.

  2. PENDING

    Playing but not acquired.

  3. REQUESTING

    Acquire in progress; acquire requests are rejected from this state onward.

  4. ACTIVE

    Acquired and playing.

  5. SUSPENDING

    Release in progress.

Source map. A teaching model of transport_state_t in BlueZ 5.78, not a captured Bluetooth session; the broadcast-sink state is omitted and transitions are simplified.

From BlueZ 5.78 profiles/audio/transport.c, acquire, GPL-2.0-or-later SPDX license; declarations and later resume logic omitted:

c
	if (transport->owner != NULL)
		return btd_error_not_authorized(msg);

	if (transport->state >= TRANSPORT_STATE_REQUESTING)
		return btd_error_not_authorized(msg);

The first condition rejects this request when a transport owner is already present. The second rejects it when the transport state has reached the requesting threshold. Both return an authorization error in this implementation; neither statement returns a usable fd. The surrounding function creates an owner, starts a resume request, frees the owner on an immediate failure, and otherwise associates the pending request and owner with the transport. The final reply can arrive asynchronously. A client must distinguish a successful method submission from receiving an fd and must handle an error or owner disappearance before using it.

The receiver owns its received fd under the D-Bus binding's transfer rules and must close it when finished. Duplicating it creates another descriptor reference; closing one numeric fd does not close all duplicates. A pending poll or worker must stop using an fd before another thread closes and possibly reuses that number. A remote disconnect can make reads or writes fail even while the local fd remains open. Release the transport and close owned descriptors according to the API, cancel pending calls where appropriate, and avoid issuing release on a new transport merely because it reused an old object path or ID.

In profiles/audio/transport.c, BlueZ 5.78, GPL-2.0-or-later SPDX (surrounding code omitted):

c
static DBusMessage *release(DBusConnection *conn, DBusMessage *msg,
					void *data)
{
	struct media_transport *transport = data;
	struct media_owner *owner = transport->owner;
	const char *sender;
	struct media_request *req;
	guint id;

	sender = dbus_message_get_sender(msg);

	if (owner == NULL || g_strcmp0(owner->name, sender) != 0)
		return btd_error_not_authorized(msg);

A release from a sender other than the current owner, or a release when no owner exists, gets an authorization error. The excerpt stops before the pending-request branch, so it does not show what happens to a descriptor already handed to the owner.

PipeWire and policy roles

PipeWire's BlueZ SPA plugin integrates supported Bluetooth transport and codec mechanisms into PipeWire objects. WirePlumber can monitor and apply policy to the resulting devices, profiles and nodes. BlueZ handles Bluetooth service and transport interfaces; PipeWire supplies media processing and graph integration; WirePlumber can choose policy. Their exact responsibilities and enabled features vary by version and configuration. A device being visible to BlueZ does not guarantee that PipeWire has created a usable node or that policy has linked an application stream.

In PipeWire 1.2.0 spa/plugins/bluez5/bluez5-dbus.c, the transport acquisition code maintains an acquire_refcount and an acquired state. Multiple users of a transport need coordinated references; the final release is different from dropping one local user's reference. Cancellation of a pending D-Bus call and release of the transport fd are also separate operations. A disconnect can race with a pending acquire callback, so a component needs to check whether the result still belongs to the current transport generation and dispose of a late fd if it no longer needs it.

Profiles, codecs and latency

A2DP is commonly used for higher-quality one-way audio distribution. Its endpoints negotiate codec capabilities and a compatible configuration; packetization and transport MTU constraints differ from raw PCM frames. SBC is a mandatory A2DP codec in relevant classic Bluetooth profiles, but the actual supported parameters and selected mode depend on both endpoints and implementation. Other codecs may be available only with suitable plugins, build options, device support and policy. This lesson does not assert patent or licensing status for any codec.

HFP and HSP are telephony-oriented profiles, commonly using a synchronous voice transport such as SCO or eSCO with their own codec and routing constraints. They are not interchangeable with an A2DP music stream merely because the same headset supports both. Switching profile can replace nodes, change available directions and change latency or sample format. A microphone path may only appear under a particular profile. The policy manager and client must tolerate profile changes and object removal rather than hold stale port pointers.

LE Audio, BAP and LC3 involve additional profiles, codecs and version-dependent implementation and controller requirements. Their existence in a specification or a source tree does not prove support in a particular BlueZ build, PipeWire plugin, controller firmware or remote device. Some paths need additional kernel, userspace and hardware capabilities. A compatibility table would require testing a specified set of versions and devices; none is claimed here.

Worked configuration and disconnect

Consider a synthetic SBC negotiation described only by symbolic capabilities. Endpoint A offers sample rates {44100, 48000} and endpoint B offers {48000}; both support stereo under this hypothetical example. Their intersection contains 48,000 and stereo, so a policy can choose that configuration if all other required codec parameters also overlap. This is a set illustration, not a real SBC capability byte sequence or a complete codec validator. If the sets are disjoint, selection must fail or another supported codec/profile must be chosen; inventing a configuration outside the intersection would not make the devices compatible.

After selection, BlueZ's configuration path creates a transport and sends SetConfiguration to the endpoint. On success, a transport can become available for acquisition under its state and authorization rules. A client requests acquire; while pending it owns a pending call, not a media fd. If it receives a successful reply with fd F and MTUs, it owns F under its binding's rules and starts the appropriate data worker. If the remote device disconnects, the worker observes failure or shutdown, stops using F and closes its owned reference; the policy and BlueZ object lifecycle determine subsequent release and removal. A ClearConfiguration or owner-change event must not race with a worker still using freed transport state.

Configure, acquire, release
Configure, acquire, release1. BlueZ daemon to Endpoint owner: SetConfiguration(transport, properties). 2. Endpoint owner to BlueZ daemon (return): configuration accepted. 3. Application to BlueZ daemon: Acquire(). 4. BlueZ daemon to Application (return): fd with read and write MTUs. 5. BlueZ daemon to BlueZ daemon: disconnect or owner loss ends the transport. 6. Application to BlueZ daemon: Release(), then close the owned fd.ApplicationBlueZ daemonEndpoint ownerSetConfiguration(transport, properties)configuration acceptedAcquire()fd with read and writeMTUsdisconnect orowner loss endsthe transportRelease(), then closethe owned fd
  1. BlueZ daemonEndpoint owner

    SetConfiguration(transport, properties)

  2. Endpoint ownerBlueZ daemon

    Return. configuration accepted

  3. ApplicationBlueZ daemon

    Acquire()

  4. BlueZ daemonApplication

    Return. fd with read and write MTUs

  5. BlueZ daemonBlueZ daemon

    disconnect or owner loss ends the transport

  6. ApplicationBlueZ daemon

    Release(), then close the owned fd

Time runs downward. Dashed arrows show returns.

Read the text version
  1. BlueZ daemon → Endpoint owner: SetConfiguration(transport, properties)
  2. Endpoint owner → BlueZ daemon (return): configuration accepted
  3. Application → BlueZ daemon: Acquire()
  4. BlueZ daemon → Application (return): fd with read and write MTUs
  5. BlueZ daemon → BlueZ daemon: disconnect or owner loss ends the transport
  6. Application → BlueZ daemon: Release(), then close the owned fd
Conceptual model. Method names follow the BlueZ 5.78 D-Bus documentation; the order is a simplified model, not a captured Bluetooth session.

Now suppose the user cancels while the acquire call is pending. Cancellation requests that local asynchronous work stop, but a reply can already be in flight. The callback must check whether its operation is still current. If a late success supplies an fd no longer needed, close that received fd and release its reply and transport references rather than leaking it or starting audio after cancellation. If the call fails before yielding an fd, do not close an unrelated integer that happens to have the same value. A reconnect creates a new lifecycle even if the displayed device name is unchanged.

Context and resource boundaries

BlueZ D-Bus methods execute in the daemon's event-driven context; PipeWire's graph can have control and real-time data loops. A callback on one side cannot assume that data processing on another thread has stopped merely because an object was unexported. Stop or synchronize workers, cancel pending calls, unregister listeners and release transport references in the order required by the relevant API. A socket fd can become readable and then fail before a worker reads; readiness is a hint, not a promise of a complete codec packet.

The transport's read and write MTUs limit packet sizes for its protocol. A media implementation must respect framing, sequence and codec-specific constraints, including partial or failed socket operations. A raw PCM frame count cannot be converted into a Bluetooth packet count by dividing by an MTU without considering codec framing and headers. Latency includes codec framing, buffering, radio scheduling, retransmission or concealment where applicable, graph processing and device behavior. These components vary; the capability model below does not measure any of them.

Object removal and reacquisition

A transport owner can disappear because its D-Bus connection closes, even if the Bluetooth device remains connected. The daemon and client need to release ownership and pending requests under their respective lifecycle rules; another client must make its own authorized acquisition rather than use the first client's stale descriptor number. If the device reconnects, capabilities or negotiated configuration may have changed. Treat the new transport as a new state machine, check its properties and MTUs again, and do not carry an old codec packetization assumption into it. This matters even when a user-facing headset name looks unchanged.

A selected audio profile does not guarantee that a transport remains available for the duration of playback. A disconnect or adapter power transition can invalidate a pending request while a client still holds a proxy object. The application must distinguish that proxy's lifetime from the remote device's availability and handle a later error or state change. This is a lifecycle example, not an observed Bluetooth connection.

Practice#

This private model computes a rate intersection and transfers a self-authored marker through an ordinary pipe. It does not open a Bluetooth device or contact D-Bus:

sh
python3 - <<'PY'
import os
left, right = {44100, 48000}, {48000}
print('shared rates:', sorted(left & right))
r, w = os.pipe()
os.write(w, b'ok')
os.close(w)
print('private pipe:', os.read(r, 8))
os.close(r)
print('disjoint:', sorted({44100} & {48000}))
PY

Expected output reports [48000], b'ok', and an empty disjoint intersection. The pipe is a descriptor-lifetime illustration, not an acquired Bluetooth transport. As a source-only alternative, use the pending-acquire timeline above and list the owner of the pending call, received fd and transport at each step. Explain why the late-success case closes the fd and why an empty intersection cannot be called a negotiated configuration.

Knowledge check#

  1. How do discovery, pairing, trust and connection differ?
  2. What does the quoted BlueZ configuration error path destroy?
  3. What do the transport's owner and requesting-state checks prevent?
  4. Does a successful acquire request immediately prove an fd has arrived?
  5. Why is an A2DP transport MTU not a PCM frame count?

Answers#

  1. Discovery finds devices, pairing establishes a security relationship, trust is a policy property, and connection establishes a link or profile. One does not automatically imply all others.
  2. It destroys the newly created transport when it cannot allocate the D-Bus method call, then reports failure.
  3. The shown implementation rejects acquisition when an owner exists or a request is already in the relevant state, returning an authorization error.
  4. No. The request can be asynchronous; the client must receive and validate the successful reply and its descriptor.
  5. The MTU bounds transport packets; encoded codec frames, headers and raw PCM frames have different representations and sizes.

Checkpoint#

Submit the symbolic capability intersection and the pending-acquire, late-reply and disconnect ownership table. Pass when the fd is closed exactly once by its owner, a pending call is not called an acquired transport, and the example is labeled synthetic. If not, reread both BlueZ paths and repeat the state table. This is practice, not certification.

Sources#

See BlueZ media endpoint implementation, BlueZ transport implementation, PipeWire BlueZ integration, and BlueZ agent interface. No Bluetooth service or device was accessed.

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.