Skip to content

[Android] Re-publishing a previously-unpublished LocalAudioTrack blocks ~20s and returns false #1013

Description

@hiropo

Environment

  • SDK: io.livekit:livekit-android:2.28.1 (latest released version as of 2026-09-01)
  • Server: livekit-server --dev v1.13.6 (local dev server)
  • Test device: Android Emulator, API 36 (not a physical device)
  • Verification method for this report: source read of the 2.28.1 tag on GitHub
    (LocalParticipant.kt / RTCEngine.kt). The ~20s hang was measured on wall-clock
    timing against the same SDK version in an earlier test run; the source read below
    confirms the code path that produces that specific duration (a 20-second deadline)
    still exists in 2.28.1.

What happens

Calling unpublishTrack() on a LocalAudioTrack and then calling
publishAudioTrack() again with that same track instance blocks for approximately
20 seconds and then returns false. No exception is thrown.

Expected vs actual

  • Expected: either re-publishing the same track instance succeeds, or it fails
    fast (immediately, or within normal RTT to the signaling server) with a clear error
    indicating why re-publish of the same instance isn't supported.
  • Actual: the call blocks for ~20 seconds before silently returning false.

Repro

val t = localParticipant.createAudioTrack()
localParticipant.publishAudioTrack(t)          // succeeds
localParticipant.unpublishTrack(t, stopOnUnpublish = false)
localParticipant.publishAudioTrack(t)          // same track instance `t`
// -> blocks ~20s, then returns false. No exception thrown.

Behavior is identical with stopOnUnpublish = true on the unpublish call.

Suspected source location

LocalParticipant.publishAudioTrack()publishTrackImpl()requestAddTrack()
engine.addTrack(cid = cid, ...).

engine.addTrack (in RTCEngine.kt) sends an AddTrackRequest to the signaling
server and then suspends on the server's confirmation:

return withDeadline(20.seconds) {
    suspendCancellableCoroutine { cont ->
        synchronized(pendingTrackResolvers) {
            pendingTrackResolvers[cid] = cont
        }
        ...
        client.sendAddTrack(cid = cid, name = name, type = kind, stream = stream, builder = builder)
    }
}

The 20-second figure matches this hardcoded withDeadline(20.seconds) exactly, which
is a strong indicator that the confirmation for this specific AddTrackRequest is
never received, and the call runs out the full deadline before failing.

cid here is track.rtcTrack.id() — the underlying WebRTC track's id, which does not
change across unpublish/re-publish of the same LocalAudioTrack instance. Our reading
is that when the same cid is submitted a second time, either:

  • the server does not send a fresh confirmation for a cid it has already seen once
    on that connection (or sends one that no longer matches the currently-pending
    resolver keyed by that cid), or
  • some client-side state tied to the cid (e.g. a stale entry) prevents the new
    request from resolving.

This part (why the specific cid re-use causes the confirmation to never arrive) is
our best-effort inference from the client source; it is not confirmed from server-side
logs or wire captures, since that would require server-side instrumentation we did not
have set up. We're flagging it as the most likely mechanism rather than a confirmed
root cause.

Impact

Any app that wants to cheaply re-arm a mic track after unpublish (instead of paying
the cost of creating a brand-new track) hits this ~20s stall on every re-publish
attempt. In our case this forced a workaround of creating a fresh LocalAudioTrack
per publish cycle — which then runs into the transceiver-leak behavior described in a
companion report, since every fresh track means a fresh transceiver that is never
released on unpublish.

Workaround

Create a fresh LocalAudioTrack via createAudioTrack() for every publish cycle
instead of reusing the same instance after unpublish.

Question for maintainers

Is re-publishing the same LocalAudioTrack instance after unpublishTrack() an
intentionally unsupported operation? If so, could publishAudioTrack() detect this
case and fail fast (or throw) instead of waiting out the full add-track deadline? If
it's meant to be supported, a repro with server-side signaling logs can be provided on
request.


Related: #1012 — the documented workaround for this issue (create a fresh LocalAudioTrack per publish) runs into that leak on every cycle, so the two are effectively two sides of the same problem.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions