Sink

A Sink is one playback endpoint as a Kubernetes resource: an analog jack, an HDMI or DisplayPort output, the playback side of a USB card, or a Bluetooth speaker. The operator creates one for every endpoint it publishes, cluster-scoped like a Node, named by the same device name the ResourceSlice carries. You never create or delete one. The operator writes status: where the endpoint is, the controls the card declares, and the values it last read. The media operator writes status.session, the volume asks of a remote’s keys. You write spec, which states the settings you want for the endpoint.

apiVersion: audio.liken.sh/v1alpha1
kind: Sink
metadata:
  name: node-1-usb-0573-1573-a34004801402-usb-audio
spec:
  volume:
    level: 80
    max: 100
    step: 5
  controls:
    PCM Playback Volume: "120"
status:
  node: node-1
  location: "1-6"
  connectionType: usb
  card:
    number: 1
    id: HID
    driver: USB-Audio
    name: USB Audio and HID
  pcm:
    device: 0
    id: USB Audio
  nodeName: liken.audio.card1-pcm0
  layoutSource: ChannelMap
  capabilities:
    PCM Playback Volume:
      type: integer
      min: 0
      max: 127
      minDecibels: "-63.50"
      maxDecibels: "0.00"
      channels: 2
    PCM Playback Switch:
      type: boolean
      channels: 1
  observed:
    volume: 80
    mute: false
    controls:
      PCM Playback Volume: "120"
      PCM Playback Switch: "on"
  conditions:
    - type: Connected
      status: "True"
    - type: Ready
      status: "True"
    - type: LayoutApplied
      status: "True"

The claim and the Sink answer two different needs. A pod that plays sound still holds the output through a claim, as the claim guide shows, and it still has its own stream volume. The Sink is for everything about the output that is not the sound itself: its default level, its mute, and the card’s own controls. Because it is an ordinary Kubernetes resource, anything with the right RBAC can change it. A pod that only wants to mute the kitchen needs no claim, and a rule that lowers every speaker at night is one patch per Sink. Set endpoint volume and controls walks through it.

One playback endpoint: an analog jack, an HDMI or DisplayPort output, a USB card’s playback side, or a Bluetooth speaker. Its status reports hardware facts and the values the operator last read. Its spec declares desired endpoint settings. The name is the machine’s name and the card’s identity, as the Sink reference gives them. When a USB card’s name would pass 63 characters, it holds the first eight hex digits of the SHA-256 of the serial in place of the serial.

spec

The desired settings for the endpoint. Every field is optional. The operator writes volume.level and mute when they change and when the endpoint appears, such as a speaker that reconnects or a node that PipeWire builds again, and otherwise reports the level the endpoint holds. It writes a declared control or codec back when the endpoint diverges from it. It never writes a field that the spec leaves out.

Field Type Required Description
volume object no The endpoint’s volume, in percent of unity. level is the default level that the operator applies. max and step bound the volume asks the media operator writes into status.session, and the operator does not read them.
mute boolean no Whether the endpoint is silent. The operator applies this setting at the same layer as volume.level, and on the same terms.
controls map[string]string no The card’s own controls, keyed by the kernel’s control name as status.capabilities lists them, such as Master Playback Volume. An integer control takes a number within its range. A boolean control takes on or off. An enumerated control takes one of its values. The operator writes a control only when spec states it. When two endpoints share one control, the last write wins because the hardware has one register.
layout []string no The channel positions of an ALSA sink, in PCM slot order, in PipeWire’s channel names, such as FL, FR, FC, LFE, RL, RR, SL, SR, RLC, RRC, TFL, TFR, NA for a slot that plays nothing, and AUX0 to AUX63. A 7.1 receiver on HDMI takes [FL, FR, RL, RR, FC, LFE, SL, SR], and a 5.1 one takes [FL, FR, RL, RR, FC, LFE]. When this field is absent, the operator selects the layout from the monitor’s ELD on HDMI and DisplayPort, from the device’s own channel map on USB, and declares no positions on the analog jack, whose streams then play in stereo. The operator declares the layout to PipeWire, and a change restarts PipeWire inside its container once no stream plays on the machine. The number of positions must be a channel count the device accepts, or PipeWire ignores the layout. The operator does not write the kernel’s channel map, so on HDMI each slot reaches the speaker the kernel’s standard allocation gives it, and a height position names a slot that the kernel routes elsewhere. A Bluetooth speaker ignores this field.
codec string no The A2DP codec to apply when no claim allocates the Bluetooth speaker. The value must be one of status.bluetooth.codecs. A claim’s codec parameter takes precedence while the claim allocates the speaker. A change here waits until the claim ends because switching codecs replaces the speaker’s node and interrupts playback. The operator ignores this field on an ALSA endpoint.

spec.volume

The endpoint’s volume, in percent of unity. level is the default level that the operator applies. max and step bound the volume asks the media operator writes into status.session, and the operator does not read them.

Field Type Required Description
level integer no The endpoint’s default level, as a percent of unity, applied to every channel alike. On an ALSA endpoint it is the gain PipeWire applies in software. On a Bluetooth speaker it is the speaker’s own volume, sent over AVRCP when the speaker supports absolute volume, and a software gain when it does not. The operator applies it when it changes and when the endpoint appears, under a claim or not, and a claim holder’s own stream fader is a separate level above it. A level that changes after that, at the device or by a volume ask, stays, and status.observed.volume reports it. When it is absent, a node that PipeWire builds while the operator runs starts at unity.
max integer no The highest level a volume ask can set. The media operator holds its asks at or below it. A person at the device can still set a higher level. Default: 100.
step integer no How far one press of a remote’s volume key moves the level the media operator asks for. Default: 5.

status

What the hardware declares and what the operator last read. The operator owns every field here apart from session, which the media operator owns.

Field Type Required Description
session object no The unit that uses the Sink, and the last volume ask a press made. The media operator writes this block by server-side apply under its own field manager. The operator writes its own status fields as a merge patch that never names this block, so its writes leave the block in place. It is status and not spec because no person declares it, and a status write changes no metadata.generation.
node string no The machine that holds the endpoint now. For a Bluetooth speaker, the value changes when the speaker moves. The name of every other Sink starts with this machine’s name, so a card that moves to another machine gets a new Sink. The operator on each machine lists and watches the Sinks by this field.
location string no Where the card is on the machine, in the kernel’s spelling: a PCI address such as 0000:00:1f.3, or a USB port path such as 1-6. Absent on a Bluetooth speaker.
connectionType string no How sound leaves the machine. One of: analog, hdmi, displayport, usb, bluetooth.
card object no The ALSA card the endpoint is on. The number and the id are this boot’s, and a second card can change both, so nothing durable is keyed to them.
pcm object no The PCM device the endpoint plays through.
monitor object no The monitor an HDMI or DisplayPort slot feeds, from the ELD the graphics driver wrote into the card. Absent while no monitor answers, and absent on every other connection type. An Intel HDMI codec binds a pin to the first free slot when a monitor appears, so a card with two monitors can swap which slot feeds which between plug events, and this object is where that shows.
bluetooth object no The speaker behind a Bluetooth endpoint. Absent on every other connection type.
nodeName string no The PipeWire node a consumer’s streams target, the same value a prepared claim delivers as PIPEWIRE_NODE. Absent while PipeWire holds no node for the endpoint.
format object no The format the node runs at, from PipeWire’s own Format parameter. Absent while the node is not running.
capabilities map[string]object no The card’s own controls for this endpoint, keyed by the kernel’s control name. A Playback control applies to the card’s analog and USB sinks. A control with no direction, such as Auto-Mute Mode, also applies to those sinks. An IEC958 Playback Switch applies to the HDMI slot with the same ordinal. It is the only control an HDMI slot lists because an HDMI PCM has no volume element. The operator omits read-only jack controls because they feed the Connected condition. A Bluetooth speaker lists no card controls.
layout []string no The channel positions the operator declares the sink’s node with, in PCM slot order. Absent when layoutSource is ChannelMap, because PipeWire reads the positions from the device, and absent when it is None. While LayoutApplied is False with the reason Restarting, this is the layout PipeWire loads when its container starts again.
layoutSource string no Where the layout came from. Spec is spec.layout. ELD is the monitor’s speaker allocation, capped by the largest LPCM channel count it accepts. ChannelMap is the channel map a USB device describes. None is no positions, and a multichannel stream then plays in stereo. An HDMI sink keeps a layout from the ELD while its monitor is off. Absent on a Bluetooth speaker. One of: Spec, ELD, ChannelMap, None.
observed object no The last value the operator read for each setting. The operator reads the card’s control device for every event it receives. It reads PipeWire’s graph for every change PipeWire reports. A change from a physical knob or a client therefore appears here without polling.
claim object no The claim that currently allocates the endpoint. This field is absent when no claim allocates it. It identifies the workload that has the speakers.
conditions []object no Connected reports whether the endpoint can play now. It is true for a monitor on an HDMI slot, a plug in an analog jack, a connected speaker, and every USB endpoint. Ready reports whether PipeWire has a node for the endpoint. These two conditions expose the same facts as the device’s no-monitor and no-sink taints, in a form a person can read. LayoutApplied, on a sink of a sound card, reports whether PipeWire runs the sink with the layout the operator selected. It is False with the reason AwaitingIdle while a new layout waits for every stream on the machine to end, and with the reason Restarting while PipeWire restarts in its container to load it.

status.session

The unit that uses the Sink, and the last volume ask a press made. The media operator writes this block by server-side apply under its own field manager. The operator writes its own status fields as a merge patch that never names this block, so its writes leave the block in place. It is status and not spec because no person declares it, and a status write changes no metadata.generation.

Field Type Required Description
player string no The Player whose unit uses the Sink, as namespace/name.
volumeAsk object no The last level a press asked for. The operator applies each new ask once, to the node or to a Bluetooth speaker’s Route, the same write that spec.volume.level takes, without waiting for the settle window that gathers hardware events into one ResourceSlice write. When several asks arrive before the operator applies one, it applies only the newest. It applies no ask that it finds the first time it reads the Sink, as in its first pass after a start, and no ask for an endpoint that has no node. It never writes an ask’s level again, so a later press of a speaker’s own button stays, and status.observed reports the level the endpoint holds.

status.session.volumeAsk

The last level a press asked for. The operator applies each new ask once, to the node or to a Bluetooth speaker’s Route, the same write that spec.volume.level takes, without waiting for the settle window that gathers hardware events into one ResourceSlice write. When several asks arrive before the operator applies one, it applies only the newest. It applies no ask that it finds the first time it reads the Sink, as in its first pass after a start, and no ask for an endpoint that has no node. It never writes an ask’s level again, so a later press of a speaker’s own button stays, and status.observed reports the level the endpoint holds.

Field Type Required Description
level integer yes The level, as a percent of unity.
mute boolean no Whether the endpoint is silent. Absent means not muted.
at string yes When the press made the ask, with milliseconds. Each new time is one ask.

status.card

The ALSA card the endpoint is on. The number and the id are this boot’s, and a second card can change both, so nothing durable is keyed to them.

Field Type Required Description
number integer no The card number the kernel assigned this boot.
id string no The card’s short id, such as PCH, with the suffix the kernel appends on a clash, such as PCH_1.
driver string no The kernel driver that binds the card, such as HDA-Intel.
name string no The card’s name, as the driver states it, such as HDA Intel PCH.

status.pcm

The PCM device the endpoint plays through.

Field Type Required Description
device integer no The PCM device number on the card, this boot.
id string no The driver’s name for the PCM, such as HDMI 0 or USB Audio. It is the part of the endpoint’s name that outlives the number.

status.monitor

The monitor an HDMI or DisplayPort slot feeds, from the ELD the graphics driver wrote into the card. Absent while no monitor answers, and absent on every other connection type. An Intel HDMI codec binds a pin to the first free slot when a monitor appears, so a card with two monitors can swap which slot feeds which between plug events, and this object is where that shows.

Field Type Required Description
display string no The name of the Display the display operator publishes for the same monitor, which is the pairing identity monitor.liken.sh/id.
manufacturer string no The monitor’s three-letter PNP id, such as GSM.
product string no The monitor’s product code, four lowercase hexadecimal digits.
name string no The monitor’s name, from the same EDID descriptor the Display reports as model.

status.bluetooth

The speaker behind a Bluetooth endpoint. Absent on every other connection type.

Field Type Required Description
address string no The speaker’s address, uppercase with colons.
name string no The name the speaker reports for itself.
peripheral string no The name of the Peripheral the bluetooth operator publishes for this speaker, which is the address in lowercase with dashes.
codec string no The A2DP codec the transport negotiated, present while the speaker is connected.
codecs []string no Every codec the speaker and this image both support, the one playing first.

status.format

The format the node runs at, from PipeWire’s own Format parameter. Absent while the node is not running.

Field Type Required Description
rate integer no The sample rate in hertz.
channels integer no The channel count.
positions []string no The channel positions in order, in PipeWire’s names: FL, FR, FC, LFE, RL, RR.

status.capabilities.*

The card’s own controls for this endpoint, keyed by the kernel’s control name. A Playback control applies to the card’s analog and USB sinks. A control with no direction, such as Auto-Mute Mode, also applies to those sinks. An IEC958 Playback Switch applies to the HDMI slot with the same ordinal. It is the only control an HDMI slot lists because an HDMI PCM has no volume element. The operator omits read-only jack controls because they feed the Connected condition. A Bluetooth speaker lists no card controls.

Field Type Required Description
type string no What the control takes. One of: integer, boolean, enumerated.
min integer no The smallest number an integer control accepts.
max integer no The largest number an integer control accepts.
step integer no The step between numbers an integer control accepts, absent when every number in the range is accepted.
minDecibels string no The level at min, in decibels, when the control declares one, as a decimal string such as -65.25. The string form keeps the value exact where a float would not.
maxDecibels string no The level at max, in decibels, when the control declares one.
values []string no Every value an enumerated control accepts.
channels integer no How many channels the control carries. A write from spec sets every channel to the same value.

status.observed

The last value the operator read for each setting. The operator reads the card’s control device for every event it receives. It reads PipeWire’s graph for every change PipeWire reports. A change from a physical knob or a client therefore appears here without polling.

Field Type Required Description
volume integer no The level the endpoint plays at, as a percent of unity, from the node’s gain or the Bluetooth device’s own volume. A suspended node reports no current level of its own: PipeWire applies a write to it without announcing it, and the node prints no level or the level it last ran at. So a suspended endpoint reports the level the operator last wrote to it, from the spec or a volume ask, which is the level it will run at. With no such write since the node last ran, it reports what the node prints.
mute boolean no Whether the endpoint is silent, on the same terms as volume.
codec string no The codec a Bluetooth speaker plays with now.
controls map[string]string no The value of every control in capabilities, in the same spelling spec.controls takes. A control with several channels reports the first channel’s value.

status.claim

The claim that currently allocates the endpoint. This field is absent when no claim allocates it. It identifies the workload that has the speakers.

Field Type Required Description
namespace string no
name string no

status.conditions[]

Connected reports whether the endpoint can play now. It is true for a monitor on an HDMI slot, a plug in an analog jack, a connected speaker, and every USB endpoint. Ready reports whether PipeWire has a node for the endpoint. These two conditions expose the same facts as the device’s no-monitor and no-sink taints, in a form a person can read. LayoutApplied, on a sink of a sound card, reports whether PipeWire runs the sink with the layout the operator selected. It is False with the reason AwaitingIdle while a new layout waits for every stream on the machine to end, and with the reason Restarting while PipeWire restarts in its container to load it.

Field Type Required Description
type string yes
status string yes One of: True, False, Unknown.
reason string yes
message string no
lastTransitionTime string yes

The name

The name is built from the hardware’s own identity, so it survives a reboot and a second card:

Endpoint Form Example
onboard PCI card node, PCI address, PCM id node-1-pci-0000-00-1f-3-hdmi-0
USB card with a serial node, vendor, product, serial, PCM id node-1-usb-0573-1573-a34004801402-usb-audio
USB card with a serial, past 63 characters node, vendor, product, serial hash, PCM id studio-screen-12-usb-046d-0a44-5fe3de05-usb-audio
USB card with no serial node, USB port path, PCM id node-1-usb-1-6-usb-audio
Bluetooth speaker address 7c-66-ef-01-23-45

The PCM id is the driver’s name for the endpoint, HDMI 0 or USB Audio, lowercased with dashes. Every ALSA form starts with the node, because a USB serial is not unique across machines: dongles of one model can all report the same serial. The serial tells apart two identical dongles on one machine. A card that moves to another machine becomes a new Sink, and so does a card with no serial that moves to another port. The old Sink stays, with its spec, and the operator on the old machine sets its Connected condition to False. Delete it when you no longer need its declaration. A card that plays and records through one PCM gives its Source the same name with -capture on the end.

A name is a DNS label, so it holds at most 63 characters. A USB serial is often 20 characters or more, so on a machine with a longer name the serial form can pass that. Then the name holds the first eight hex digits of the serial’s SHA-256 in place of the serial, and keeps the rest. The hash is stable and differs for each serial, so two identical dongles on one machine still get two names. The operator tests each name alone, and -capture adds eight characters. So a dongle whose Sink name is 56 to 63 characters keeps the serial in the Sink name and holds the hash in the Source name. To compute the hash by hand:

printf %s ABCDEF0123456789ABCDEF0 | sha256sum | cut -c1-8

The operator refuses any other name that passes 63 characters, and its log names the endpoint and the length.

On an Intel HDMI codec, hdmi-0 names the card’s first HDMI slot and not a physical port. A pin binds to the first free slot when a monitor appears, so on a card with two monitors the slot each one lands in can change between plug events. status.monitor reports which monitor the slot feeds now, and a machine with one HDMI monitor never sees the difference.

How the operator applies the spec

The operator writes a declared volume.level and mute when the declaration changes and when the endpoint appears: a speaker that reconnects, or a node that PipeWire builds again. At every other time it follows the device. A press of a speaker’s own button, a client that changes the graph, and a volume ask move the level, and the operator reports the new level in status.observed and does not write the declaration back. A declared control and a declared codec are standing instructions: the operator compares each one with the value it last read, and writes the hardware only where the two diverge. A declared control is validated against status.capabilities: a name the card does not declare, or a value out of its range, fails the pass with the reason in the operator’s log and is never written. An empty spec writes nothing at all. The operator invents no value: an endpoint with no declarations keeps whatever the hardware holds, except that every sink starts at unity gain so that no hidden multiplier costs resolution before the codec runs. The unity write goes only to a node PipeWire builds while the operator runs. After a restart, the operator writes nothing to a sink that was there before it started, and the sink keeps the level it holds.

A restart writes no declared level either. The level an endpoint holds when the operator starts can be a volume ask or a press of the speaker’s button that the declaration does not know, and an idle node reports no level, so the operator cannot read whether it already holds the declaration. The first pass treats the declaration as the level each endpoint holds, and a later change of the declaration is written at once.

volume.max and volume.step bound the volume asks below, and the operator writes neither to the hardware. Each one has a default, 100 and 5 percent, which the API server fills in when spec.volume is present.

volume.level, mute, and controls apply at once, whether a claim holds the endpoint or not. codec waits for the claim to end, because a codec switch replaces the speaker’s node and interrupts playback, and a claim’s own codec parameter wins while it holds the speaker.

The volume asks

A remote’s volume key reaches a Sink through the media operator. It turns each press into an absolute level, at most volume.max, and writes it into status.session.volumeAsk with the time of the press. It writes that block by server-side apply under its own field manager, and this operator writes the rest of status as a merge patch that never names it, so neither writer removes the other’s fields.

status:
  session:
    player: media/den
    volumeAsk:
      level: 45
      mute: false
      at: "2026-10-04T12:15:25.164Z"

Each new at is one ask, and the operator applies it once, with the same write that volume.level takes: the node’s gain, or a Bluetooth speaker’s own volume over AVRCP. The ask does not wait for the 1.5 second settle window that gathers a burst of hardware events into one ResourceSlice write, because a level write changes no ResourceSlice. When several asks arrive before the operator applies one, it applies only the newest. When the write lands, the operator writes the asked level and mute into status.observed at once, and the next pass replaces them with what PipeWire reports. It applies no ask that it finds the first time it reads the Sink, as in its first pass after a start, because the last operator applied that ask, or the device’s level is newer than it. An ask for an endpoint with no node is dropped, and the endpoint takes volume.level when it appears.

Observation

status.observed follows two event sources and no timer. The card’s control device reports every control write from any process, every jack change, every monitor change, and a knob turned on a USB DAC. PipeWire’s graph reports every node and device change. So a change a person made with a knob, a remote, or a speaker’s own buttons shows in observed within about a second. A declared control or codec is written back on the same event, and a declared level is not.

The channel layout

The operator declares every sink of a sound card to PipeWire with a channel layout: the position of each PCM slot, in PipeWire’s channel names. WirePlumber links each channel of a stream to the sink’s channel of the same name. It links every stream to a sink with no positions as two channels, FL and FR, so a 5.1 or 7.1 stream is mixed down into them.

The operator takes the layout from the first source that gives one, and status.layoutSource names it:

Source Where the positions come from
Spec spec.layout
ELD the HDMI or DisplayPort monitor’s speaker allocation, capped by the largest LPCM channel count it accepts
ChannelMap the channel map a USB device describes in its descriptors. PipeWire reads it when it opens the device, so status.layout is absent
None nothing. The analog jack reports no speakers, and neither does an HDMI output whose monitor was off when the pod started

From the ELD, the operator selects one of the HDMI layouts that PipeWire’s own card profiles use:

The monitor advertises status.layout
8-channel LPCM and FL/FR, LFE, FC, RL/RR, RLC/RRC FL, FR, RL, RR, FC, LFE, SL, SR
6-channel LPCM and FL/FR, LFE, FC, RL/RR FL, FR, RL, RR, FC, LFE
anything else FL, FR

Most televisions accept 8-channel LPCM and have two speakers. Such a set gets FL, FR, so PipeWire mixes the center channel, which carries the dialog, into the front pair. An HDMI sink keeps its layout from the ELD while its monitor is off, so a receiver that turns off and on again changes nothing.

PipeWire reads the layout once, when it starts. When a sink’s layout changes, because spec.layout changed or a monitor with another layout answers, the operator writes the new declaration, and the PipeWire container’s first process restarts PipeWire in place, so the restart never waits in the kubelet’s crash backoff. The restart ends every stream on the machine, so the operator waits until no stream plays. LayoutApplied is False with the reason AwaitingIdle while it waits, and with the reason Restarting until the new PipeWire starts. Each change posts one LayoutChanged Event on the Sink, which names the old layout, the new one, and the source.

Two things the operator does not do:

Events

The operator posts a Kubernetes Event on a Sink for each change of a condition and for each action it takes. A Sink is cluster-scoped, so its Events are in the default namespace. kubectl describe sink shows them. kubectl events --for shows them only with -n default or -A:

kubectl events -n default --for sink/<name>

The API server deletes an Event one hour after its last write, so the conditions and the operator’s log hold the facts for longer. The operator patches the count of an Event that repeats within 10 minutes, in place of a new Event.

Each condition change posts one Event with the condition’s own reason and message, such as NoMonitor, JackEmpty, or AwaitingIdle. The first status write posts one for each condition. A change of Ready to False is a Warning while Connected is True, because sound can leave the endpoint and PipeWire holds no node to send it through. Every other condition change is Normal: a television that turns off and a plug pulled from a jack are things a person does.

The operator posts these reasons for the actions and faults that change no condition:

Reason Type When
LayoutChanged Normal The operator wrote a new channel layout, and PipeWire restarts in its container to apply it.
LayoutWriteFailed Warning The operator could not write the declaration that holds a new layout. The sink keeps its layout, and each pass tries the write again.
SpecRefused Warning The spec states a value the endpoint does not take, such as a codec the speaker does not offer. The message names each refused value.
PipeWireLost Warning A read of PipeWire’s graph failed. After 3 failed reads in a row, the operator taints every output and restarts.
PipeWireRecovered Normal PipeWire answers a graph read again, after one or more that failed.
BluetoothUnavailable Warning On a speaker’s Sink: bluetoothd did not answer a read of the paired speakers. The speaker publishes with a taint until it answers.
BluetoothAvailable Normal On a speaker’s Sink: bluetoothd answers again.
Captured Normal The capture API returned the audio of the Sink to a caller. The message names the caller and the format.

A fault posts once when the operator first meets it, not once for each pass that meets it again.