Skip to content

iOS: audio publish frame-watcher timeout (code 101) leaves an orphan track — server keeps the publication, client has no LocalTrackPublication, mute becomes a no-op #1098

Description

@iostiny

Summary

When LocalParticipant._publish fails at the frame-watcher step (local audio track produces no PCM frame within 5s → LiveKitError(.timedOut), code 101), the track has already been added server-side via AddTrack and negotiated, but add(publication:) is never reached. The catch block only calls track.stop(), and LocalAudioTrack.stopCapture() merely cleans up the frame watcher. Result: an orphan track — the SFU keeps a live publication and the transceiver stays attached, while the client has no LocalTrackPublication for it.

Where (v2.11.0, unchanged on main as of 2.16.x)

  • Sources/LiveKit/Participant/LocalParticipant.swift
    • ~L712–719: after AddTrack + negotiate, try await track.startWaitingForFrames()
    • ~L736: add(publication:) (never reached on timeout)
    • ~L749–755: catch → try await track.stop() only
  • Sources/LiveKit/Track/Local/LocalAudioTrack.swift ~L125: AsyncCompleter<Void>(label: "Frame watcher", defaultTimeout: 5); ~L101–103: stopCapture() → cleanUpFrameWatcher() only

Impact

  1. Mute silently no-ops: setMicrophone(enabled: false) / set(source: .microphone, enabled: false) finds no publication → nothing happens, while the server-side track (and transceiver) remains. UI can show "muted" while the track is still published.
  2. isMicrophoneStreaming reports false although the SFU lists an active audio track for the participant.
  3. The app receives timedOut(101) and has no handle to the orphan track to unpublish it. Server logs show mediaTrack published at publish time and track_unpublished only when the participant leaves.
  4. From the app's point of view the failure is indistinguishable from "nothing was published", so retrying setMicrophone(enabled: true) publishes a second track.

Observed timeline (iOS 26.x, iPhone, SDK 2.11.0, protocol 16)

  • t+0.000 app calls setMicrophone(enabled: true)
  • t+0.205 SFU logs mediaTrack published (AddTrack round-trip)
  • t+5.35 client gets timedOut(101) — matches AddTrack RTT + 5s frame-watcher timeout (the 10s AddTrack completer is not what fires)
  • SFU keeps the track until participant closing ~19s later

Audio session was already activated via CallKit (provider(_:didActivate:)) ~8s before publish, so this is not the session-activation race.

Expected

Either of:

  • On frame-watcher timeout, still call add(publication:) (or return the publication in the thrown error) so the app can mute/unpublish it; or
  • In the catch, fully roll back: send RemoveTrack for the already-added SID, stop/detach the underlying mediaTrack, and remove the transceiver — so the server state matches the client state.

Related: #843 (video side, dimensionsCompleter) — that one times out before AddTrack, so it does not leave an orphan; the audio path does.

Happy to open a PR for the rollback variant if that's the preferred direction.

Activity

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

Metadata

Metadata

Assignees

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