Android AVAudioRecorder: swallowed exceptions, two leaked MediaRecorders per take, and hardcoded stereo
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 14
- Forks
- 12
- PR merge metrics
- No merged PRs in 30d
Description
Related to #28 (API divergences) but a different class of problem: resource handling and error observability in the Android AVAudioRecorder (#elseif SKIP branch, checked against 0.7.1 / 7c44621). These cost us a multi-day production incident: recording failed on a user's device with RECORD_AUDIO provably granted, and the swallowed exception made the root cause undiagnosable from logs.
1. prepareToRecord() swallows the real exception
public func prepareToRecord() -> Bool {
do {
...
recorder = MediaRecorder(context).apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
...
prepare()
}
return true
} catch {
return false
}
}
setAudioSource throws for appops blocks (vendor permission managers), a mic held by another client, etc.; prepare() throws for I/O problems. All of that collapses into false with no message, no log, no way for the app to distinguish "permission-like problem" from "encoder problem". Suggestion: at minimum log the exception; ideally expose it (a lastError accessor, or make the API throwing).
2. Two native MediaRecorder instances leak per successful take
Three call sites each build a fresh MediaRecorder, and only the last one is ever released:
init(url:settings:)callsprepareToRecord()(instance A),- an app calling
prepareToRecord()itself — as the iOS contract encourages — builds instance B (A is never released), record()callsprepareToRecord()again (instance C; B leaks),stop()releases only C.
Every successful record/stop cycle therefore leaks two native recorders; failed prepareToRecord() calls leak their partially-configured instance too. On low-end devices this exhausts media-server resources over a session. Suggestion: prepareToRecord() should release the previous instance before building a new one (or become idempotent), and the error path needs reset()/release().
3. stop() never releases when MediaRecorder.stop() throws
MediaRecorder.stop() throws RuntimeException when no valid samples were written (tap-and-release). In that case the release() on the next line is never reached and the delegate is the only signal — the native instance stays alive. A finally { recorder?.release() } would cover it. (The unfinalized output file also remains on disk; on iOS the file at url is left in a defined state, here it is an unreadable MP4 fragment.)
4. setAudioChannels(2) is hardcoded; AVNumberOfChannelsKey (and AVFormatIDKey/quality) are ignored
setAudioChannels(2)
setAudioSamplingRate(settings["AVSampleRateKey"] as? Int ?? 44100)
An app passing AVNumberOfChannelsKey: 1 (mono, the common voice-recording setup) silently records stereo on Android. Stereo MIC capture is also the least-supported path on budget devices, so this both diverges from the requested format and reduces device compatibility. Aside: AVSampleRateKey is conventionally a Double (44100.0) on iOS, so the as? Int cast fails and the key is effectively ignored as well.
Happy to provide more detail. For context, our app worked around all four by moving recording to its own small Kotlin MediaRecorder wrapper — but these fixes seem broadly useful for anyone recording through SkipAV.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in the Android AVAudioRecorder #elseif SKIP branch and trace prepareToRecord(), record(), and stop(), including the settings lookup. Done means errors are observable, native recorders are released on every path, and channel and sample-rate settings are honored without leaving invalid output files.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- audio-video-rtc, mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100