Skip to content

fix(linux): stop the takeover firing on non-events with two devices connected - #724

Open
RamfiAogusto wants to merge 2 commits into
librepods-org:linux/rustfrom
RamfiAogusto:fix/takeover-false-triggers
Open

fix(linux): stop the takeover firing on non-events with two devices connected#724
RamfiAogusto wants to merge 2 commits into
librepods-org:linux/rustfrom
RamfiAogusto:fix/takeover-false-triggers

Conversation

@RamfiAogusto

Copy link
Copy Markdown

Found while using the DID hook with an iPhone and a Linux laptop connected to the same AirPods Pro. Four defects, all in media_controller.rs, that make the takeover fire when the user did nothing — and with a second device connected, firing means pulling the audio away from a device they were listening on.

1. The audio card lookup does not wait

Taking the connection back makes the earbuds renegotiate, and the card drops out of the sound server for a moment. get_audio_device_index looked exactly once, so a takeover landing in that window found nothing and gave up.

The failure mode is worse than a missing profile: ownership has already been taken from the other device by then, so the audio ends up nowhere. Playback stops where it was working and never starts here.

taking ownership and activating a2dp
hijacking connection by asking AirPods
ERROR No matching Bluetooth card found for MAC CC:22:FE:54:1B:B1

Now it polls for up to three seconds.

2. Ear detection treats "unknown" as "removed"

old_all_out is .all() over the previous reading, which is vacuously true for an empty list. LibrePods reinitialises on every reconnect, so the first report of an already-worn pair reads as buds just inserted:

AirPods connected: ... initializing
Audio Source: <phone>, type: Media
Ear Detection Status: [InEar, InEar]
Ear Detection - old_in_ear_data: [], new_in_ear_data: [true, true]
... 11s later ...
Media playback started, taking ownership

The buds were never removed. The first reading is now recorded as a baseline.

3. Playback state has the same shape of bug

is_playing starts false as a placeholder, so anything already playing when the listener starts reads as playback that just began. A reconnect while media is open takes the audio. The first poll is now a baseline too.

4. Losing ownership feeds back into taking it

Losing ownership pauses local players and drops the audio profile; players tend to resume once the profile returns. That resume was indistinguishable from the user pressing play, so it took ownership back from the device that had just claimed it — which pauses this side again:

03:09:47  activated A2DP, hijacking
03:09:48  Audio Source: <laptop>, Media
03:09:49  Lost ownership, pausing media and disconnecting audio
03:09:50  Audio Source: <phone>, Media
03:09:50  taking ownership and activating a2dp
03:09:51  Audio Source: <laptop>, Media
03:09:59  Audio Source: 00:00:00:00:00:00, None

The two devices trade the audio until neither is playing. i_paused_the_media already existed for exactly this but was never set on that path.

Testing

Ubuntu 26.04, GNOME 50.1 Wayland, AirPods Pro 3, iPhone and laptop both connected via the DID hook (DeviceID = bluetooth:004c:... in BlueZ main.conf).

Before: restarting the service while the phone was playing took the audio within seconds, and the two devices traded it until both went silent.

After: a restart with media open leaves the phone playing untouched, and pressing play locally still takes the audio as intended.

One limitation is unchanged and out of scope here: if the other device is still actively playing, the earbuds may answer the hijack with OwnsConnection: 00 and keep it there. That is the earbuds arbitrating, not this code.

Taking the connection back from another device makes the earbuds renegotiate,
and the card drops out of the sound server for a moment while that happens.
get_audio_device_index looked exactly once, so a takeover that landed in that
window found nothing and gave up.

The failure is worse than a missing profile: ownership has already been taken
from the other device by then, so the audio ends up nowhere. Playback stops on
the device that was working and never starts on this one.

Poll for up to three seconds instead of looking once.
…tate

Three places treated an unknown initial state as an observed one, and each made
the takeover fire when nothing had happened. With a second device connected,
that means pulling the audio away from a device the user was listening on.

Ear detection: `old_all_out` is computed with `.all()` over the previous
reading, which is vacuously true for an empty list. The first report after
connecting therefore looks like the buds were just inserted even when they were
never removed — LibrePods reinitialises on every reconnect, so a worn pair
triggers a resume on each one. Record the first reading as a baseline instead.

Playback: `is_playing` starts false as a placeholder. Anything already playing
when the listener starts reads as playback that just began, so a reconnect
while media is open takes the audio. Skip the first poll for the same reason.

Self-inflicted pauses: losing ownership pauses local players and drops the
audio profile, and players tend to resume once the profile returns. That resume
was indistinguishable from the user pressing play, so it took ownership back
from the device that had just claimed it — which pauses this side again, and
the two devices trade the audio until neither is playing. `i_paused_the_media`
already existed for this but was never set on that path.

Together these stop the takeover from firing on reconnects and feedback loops,
while a genuine play still takes the audio as before.
v4rgas added a commit to v4rgas/librepods that referenced this pull request Aug 12, 2026
…onnected

From librepods-org#724 by RamfiAogusto. Four defects that make
the takeover fire when the user did nothing, which with a second device
connected means pulling audio away from whatever they were listening on:

- The audio card lookup did not wait. Taking the connection back makes
  the earbuds renegotiate and the card briefly leaves the sound server;
  a single lookup landing in that window gave up after ownership had
  already been taken, leaving the audio nowhere. Now polls for 3s.
- Ear detection read "unknown" as "removed": old_all_out is vacuously
  true for the empty list every reconnect starts with, so an
  already-worn pair looked freshly inserted. First reading is now a
  baseline.
- is_playing started false as a placeholder, so anything already playing
  when the listener started read as playback that just began. Also a
  baseline now.
- Losing ownership pauses local players and drops the profile; players
  resume by themselves when it returns, which was indistinguishable from
  the user pressing play and took the audio back from the device that
  had just claimed it. i_paused_the_media existed for this and was never
  set on that path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant