Skip to content

#1732: Fix audio timestamps when audio source discontinues. #1733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mstyura
Copy link

@mstyura mstyura commented Jun 27, 2025

Description & motivation

Fix timestamps in RTMP packets remain continuous, even when audio source is discontinuous (has gaps). This lead to lip-sync problems.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Explanation

The reset of the audio timeline when audio codec re-creates audio converter is required to pick new anchor time of next audio sample.
The anchor time update should also pick the current sample from provided anchor time, otherwise extrapolate time will jump forward or backward when computing new timestamp.

Corner case

This fixes the case when switch between devices which has different audio formats (e.g sample rate), which causes reset of AudioCodec. When two BT devices has sample format, but different clock this fix is not enough. There should be other way to trigger AudioCodec state reset.

Screenshots:

…hen audio source has discontinuations.

Continuous time stamps after compression lead to wrong delta timestamps computation in RTMP
causing lip-sync.
@mstyura
Copy link
Author

mstyura commented Jun 27, 2025

The following workaround could be done by client app for the corner case mentioned in the description:
The app could inject special audio buffer to reset states of re-sampler and codec and be actually rejected by RTMP stack before sending:

  private func preparePoisonedAudioBuffer(captureFormat audioFormat: AVAudioFormat) -> (buffer: AVAudioPCMBuffer, time: AVAudioTime)? {
    let framesCount = AVAudioFrameCount(1024)

    let poisonedSampleRate = audioFormat.sampleRate == 16000.0 ? 48000.0 : 16000.0

    guard let poisonedAudioFormat: AVAudioFormat =
      .init(commonFormat: .pcmFormatInt16,
            sampleRate: poisonedSampleRate,
            channels: audioFormat.channelCount,
            interleaved: true) else {
      logger.error("Invalid poisoned audio input format")
      return nil
    }

    guard let poisonedBuffer = AVAudioPCMBuffer(pcmFormat: poisonedAudioFormat,
                                                frameCapacity: framesCount) else {
      logger.error("Unable to create poisoned audio PCM buffer")
      return nil
    }

    poisonedBuffer.frameLength = framesCount

    poisonedBuffer
      .int16ChannelData
      .unsafelyUnwrapped[0] // SAFETY: The buffer was created with PCM int16 format
      .update(repeating: 0, count: Int(framesCount * audioFormat.channelCount))

    let poisonedTime = AVAudioTime(
      hostTime: 0xADE1A, // Will be rejected by RTMP stack as sample from the past (absolute capture time is behind what already sent)
      sampleTime: 0,
      atRate: audioFormat.sampleRate)

    return (buffer: poisonedBuffer, time: poisonedTime)
  }

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

Successfully merging this pull request may close these issues.

1 participant