Skip to content

Camera: fix round video recording with a headset microphone - #2409

Open
dallo28 wants to merge 2 commits into
TelegramMessenger:masterfrom
dallo28:fix/round-video-headset-audio
Open

dallo28 wants to merge 2 commits into
TelegramMessenger:masterfrom
dallo28:fix/round-video-headset-audio

Conversation

@dallo28

@dallo28 dallo28 commented Oct 27, 2026 •

Copy link
Copy Markdown

Fixes https://bugs.telegram.org/c/66487 (and the merged https://bugs.telegram.org/c/66930): with the modern video message pipeline, a round video never starts recording when a headset microphone is the input. The camera keeps showing the loading animation and the timer stays at 0:00. Reported with Bluetooth earbuds and with wired EarPods, both on iPhones that support multi-camera capture.

Cause

Round videos use AVCaptureMultiCamSession on every device that supports it (isDualCameraSupported(forRoundVideo: true)). In that mode CameraNeo.configureAudio() added the microphone input and the audio data output with no connections, then connected every audio port of the input to the output in one manual AVCaptureConnection.

VideoRecorder only starts writing once the first audio sample has created the AAC writer input (tryStartWriting requires !hasAudio || audioInput != nil), and nothing gives up after a timeout. The duration shown in the UI only advances once writing has started. So if the audio connection delivers no samples, the recording stays in .arming forever, which is exactly the reported symptom. A failure would close the screen instead: the screen's error handler calls completion(nil, …).

The built-in microphone exposes several positional ports in a multi-camera session, so the manual connection works there. With a headset microphone (pinned as the preferred input by ManagedAudioSession when a headset is connected) the input's ports are different, and the manual connection most likely delivers nothing. Voice messages and the legacy round-video pipeline use the same audio session and keep working with the same headsets.

A second problem sits behind the first one and only shows up once audio flows. VideoRecorder encodes AAC at a fixed 64 kbps with the microphone's own sample rate and channel count. A Bluetooth headset microphone uses the call profile, which delivers 8 or 16 kHz, and the system AAC encoder allows at most 24 kbps (8 kHz mono) or 48 kbps (16 kHz mono) there. canApply(outputSettings:) still accepts 64 kbps; startWriting() then fails with "Cannot Encode Media" (AVFoundationErrorDomain -11861, underlying -12651), and the camera closes with an error. Measured with the system AAC encoder on macOS:

Sample rate Max AAC bit rate, mono Max AAC bit rate, stereo
8 kHz 24 kbps 48 kbps
16 kHz 48 kbps 96 kbps
24 kHz 64 kbps 128 kbps
44.1 / 48 kHz 256 kbps 320 kbps

The legacy recorder doesn't hit this because it starts from recommendedAudioSettingsForAssetWriter.

Change

1. Microphone connection (CameraNeo.swift)

configureAudio() adds the microphone input and the audio output with implicit connections (addInput / addOutput) in both modes, so the session picks the port for the current route. This is what the legacy camera has always done, in the same AVCaptureMultiCamSession (CameraInput.configureAudioInput, CameraOutput.configure).

Why this doesn't affect anything else:

  • In multi-camera mode both camera inputs and their video outputs are added with no connections, and before the audio. An implicit connection only joins matching media types, so the microphone can only connect to the audio data output, and the video wiring is untouched. The preview layers are also attached with automaticallyConnect: false in multi-camera mode.
  • Single-camera mode already added the microphone this way, and camera switching there only swaps the video input.
  • Teardown removes every input and output, so it doesn't depend on how the connection was made. The media-services reset rebuilds the session through the same configureSession().
  • Frame-rate changes and the multi-camera cost tuning never touch audio, and audio doesn't count toward the multi-camera hardware cost.
  • Audio samples are still routed by output identity (audioOutputIdentity), as before.

The one change in behaviour: with the built-in microphone in multi-camera mode, the session now picks the port instead of all positional ports feeding one output. That is what the legacy pipeline records.

2. AAC bit rate (VideoRecorder.swift)

addAudioInputIfPossible asks the AAC encoder for the bit rates it accepts at the actual sample rate and channel count (kAudioConverterApplicableEncodeBitRates) and uses its maximum when that is below 64 kbps. In practice only 8 and 16 kHz mono are lowered (to 24 and 48 kbps); every other format, including the built-in microphone at 44.1/48 kHz, keeps 64 kbps. If the query fails, the settings are unchanged.

Testing

  • Builds for the simulator (debug_sim_arm64).
  • The bit-rate helper, run verbatim on macOS: it returns the limits in the table above, and AVAssetWriter starts with the capped rate at 8 and 16 kHz (mono and stereo) where 64 kbps fails. Encoding 2 s of PCM through the writer at 24 kbps / 8 kHz and 48 kbps / 16 kHz completes.
  • Not tested on a device yet. The simulator has no camera or microphone ports, so it can't exercise this path. To verify: record a round video with the modern pipeline on a multi-camera iPhone with wired EarPods, then with Bluetooth earbuds, then with no headset (check that the built-in microphone still sounds right).

Not in this PR

  • A fallback that starts writing without audio if no sample arrives within a short time. Neither pipeline has one today; it would change behaviour (a silent round video instead of a hang), so it belongs in its own change.
With the modern video message pipeline, recording a round video stays at
0:00 when a headset microphone (wired EarPods or Bluetooth) is the input
(bugs.telegram.org/c/66487, c/66930).

In multi-camera mode CameraNeo added the microphone without connections
and connected every audio port of the input to the audio data output by
hand. VideoRecorder starts writing only after the first audio sample has
created the audio track, with no timeout, so if that connection delivers
nothing the recording never starts.

Add the microphone input and the audio output with implicit connections
in both modes, as the legacy camera does in the same multi-camera
session, and let the session pick the port for the current route.
VideoRecorder encodes audio as AAC at 64 kbps with the microphone's own
sample rate and channel count. A headset microphone on the Bluetooth
call profile delivers 8 or 16 kHz, where the system AAC encoder allows
at most 24 or 48 kbps for mono. canApply(outputSettings:) still accepts
64 kbps, startWriting() then fails with "Cannot Encode Media" (-11861),
and the camera closes with an error.

Ask the encoder for the bit rates it accepts at the actual sample rate
and channel count, and use its maximum when that is below 64 kbps.
Every other format keeps 64 kbps.
@dallo28 dallo28 changed the title Camera: let the session connect the microphone for round videos Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant