Skip to content

iOS: first remote audio is silent in joined receive-only state until local microphone transmit starts audio engine #1069

Description

@aaron-olds-govcio

Describe the bug
When an iOS client joins a LiveKit room in a receive-only push-to-talk configuration, the first remote audio does not play while the iOS client is idle.

From the app’s perspective, signaling and subscription appear to work correctly:

  • the iOS app shows the remote speaker as active
  • the remote audio publication callback fires
  • the remote audio subscription callback fires

However, no audio is heard until the iOS client starts local transmit. Once local transmit starts, the previously delayed Android speech becomes audible.

This appears to be a remote playout activation issue in a joined receive-only state.

SDK Version
LiveKit Swift SDK 2.15.2

LiveKit Swift package revision: 77b5aad07909e23adf97d39f205ef7e18e2ceff5

WebRTC XCFramework version: 144.7559.11

iOS/macOS Version
iOS 26.5.2

Xcode Version
Xcode 26.6

Swift 6.3.3

Steps to Reproduce

  1. Join the same LiveKit room from Android and iOS.
  2. On iOS, connect with autoSubscribe: true and enableMicrophone: false.
  3. Keep the iOS client joined but idle. Do not press PTT on iOS.
  4. On Android, press PTT and speak.
  5. Observe that the iOS client receives active-speaker updates and appears subscribed to the remote audio track.
  6. Observe that iOS does not play the live remote audio.
  7. On iOS, start local transmit.
  8. Observe that once local transmit starts, the previously delayed Android speech becomes audible.

Examples
The problem is reproducible with:

We have not yet reproduced this in the sample apps. We currently reproduce it in our production app integration.

Expected behavior
Remote audio should begin playing immediately when a subscribed remote participant speaks, even if the iOS client is joined in a receive-only state and has not activated its local microphone.

Screenshots
Not applicable.

Logs
We added app-side instrumentation around room connect lifecycle, remote audio publication/subscription, audio engine lifecycle, audio session snapshots, and first remote PCM frame delivery.

Observed sequence from repeated repros:

  1. Room connect succeeds.
  2. iOS remains joined and idle in receive-only mode.
  3. Android transmits.
  4. iOS receives active-speaker updates.
  5. iOS logs remote audio publication.
  6. iOS logs remote audio subscription.
  7. Audio session snapshots during the idle listener state show engineRunning=false.
  8. No engine-start callback fires for remote-only playout.
  9. No remote PCM frames are rendered during the initial Android transmission.
  10. When iOS starts local transmit, engine start callbacks fire.
  11. After local transmit begins, remote PCM frames start rendering and the previously delayed Android speech becomes audible.

Relevant app behavior on join:

try await AudioManager.shared.setRecordingAlwaysPreparedMode(false)

try await room.connect(
    url: url,
    token: token,
    connectOptions: ConnectOptions(autoSubscribe: true, enableMicrophone: false)
)

Relevant receive-only listener configuration:

private func configureReceiveOnlyAudio() {
    let session = AVAudioSession.sharedInstance()
    do {
        if session.category == .playAndRecord || session.mode == .voiceChat {
            try session.setActive(false)
        }
        try session.setCategory(
            .playback,
            mode: .default,
            options: [.mixWithOthers]
        )
        try session.setActive(true)
    } catch {
        logger.error("Failed to configure receive-only audio: \(String(describing: error))")
    }
}

Relevant transmit-path engine configuration:

private func configureTransmittingEngineAvailability() {
    do {
        AudioManager.shared.isVoiceProcessingBypassed = false
        try AudioManager.shared.setEngineAvailability(.default)
    } catch {
        logger.error("Failed to configure transmitting engine availability: \(String(describing: error))")
    }
}

Exact instrumentation strings used in the app:

LiveKit remote audio published for <channel>: sid=<sid>, source=<source>, subscribed=<bool>

LiveKit remote audio subscribed for <channel>: sid=<sid>, source=<source>

LiveKit audio session snapshot [connect-post-room]: category=<category>, mode=<mode>, otherAudioPlaying=<bool>, outputVolume=<float>, outputs=<outputs>, inputs=<inputs>, engineRunning=false

LiveKit audio session snapshot [remote-audio-published-<channel>-<sid>]: category=<category>, mode=<mode>, otherAudioPlaying=<bool>, outputVolume=<float>, outputs=<outputs>, inputs=<inputs>, engineRunning=false

LiveKit audio session snapshot [remote-audio-subscribed-<channel>-<sid>]: category=<category>, mode=<mode>, otherAudioPlaying=<bool>, outputVolume=<float>, outputs=<outputs>, inputs=<inputs>, engineRunning=false

LiveKit engineWillStart: playout=<bool>, recording=<bool>, running=<bool>

LiveKit remote audio first frame for <channel>: sid=<sid>, sampleRate=<sampleRate>, frameLength=<frameLength>, channels=<channels>

This suggests the issue is not signaling or subscription failure, but remote playout activation in a joined receive-only state.

Important integration constraints for this use case:

  • this is push-to-talk, not a full-time bidirectional call
  • joined-but-idle listeners must remain in playback/media semantics
  • we do not want to keep the client in permanent .playAndRecord just to make remote audio start
  • we do not pre-create or pre-publish the local microphone on join

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions