software-mansion / software-mansion/react-native-audio-api
[Android] AudioContext.resume() fails permanently after the Oboe stream dies with a non-Disconnected error (audio silent for the rest of the session)
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 839
- Forks
- 92
- Avg merge
- 4d 15h
- Merged PRs (30d)
- 27
Description
Summary
On Android, once the Oboe stream dies in a way that AudioPlayer::onErrorAfterClose does not recover from, AudioContext.resume() fails permanently for the rest of the process. The context reports state === "suspended", every resume() call rejects with "Failed to resume audio context.", and nothing in the library ever rebuilds the stream. The app is silent until the user force-closes it.
We are seeing this in production (game built on react-native-audio-api, buffer-source SFX + music): ~20 Android users in a 14-day window with hundreds to thousands of consecutive resume failures each (worst case ~5,000 in a single stretch), across Pixel and Samsung devices.
Environment
react-native-audio-api: 0.13.2 (the wedge logic is unchanged on currentmain)- React Native: 0.83.2 (Fabric)
- Android (production builds); iOS unaffected
Root cause
- The only stream-death recovery is
AudioPlayer::onErrorAfterClose(android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp), and it is gated tooboe::Result::ErrorDisconnected:
void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) {
if (error != oboe::Result::ErrorDisconnected || driverMutex_ == nullptr) {
return;
}
...
Any other terminal error (ErrorNoService after an audioserver restart, ErrorTimeout, etc.) returns early, leaving mStream_ pointing at a stream Oboe has already closed. The same dead end is reached when the reopen inside the ErrorDisconnected path itself fails (there is no retry).
-
BaseAudioContext::getState()reportsSUSPENDEDwhenever the driver is not running, so the JS side correctly sees"suspended"and callsresume(). -
AudioContext::resume()(common/cpp/audioapi/core/AudioContext.cpp) then has no path that can succeed:
if (isInitialized_.load(std::memory_order_acquire) && audioPlayer_->resume()) {
// requestStart() on the closed stream fails -> false
...
}
return tryStartDriver();
audioPlayer_->resume() calls requestStart() on the closed stream and fails. Then tryStartDriver() immediately returns false because isInitialized_ is still true:
bool AudioContext::tryStartDriver() {
...
if (isInitialized_.load(std::memory_order_acquire)) {
return false; // <- permanent: nothing ever resets isInitialized_ or reopens the stream
}
So after any unrecovered stream death: resume() fails forever, and there is no API the app can use to recover short of tearing down the whole AudioContext and rebuilding its graph.
Repro
Hard to trigger on demand, but killing the audio server while the app is in a suspended/idle-audio state reproduces the class of failure:
- Play some audio through an
AudioContext, let the context become suspended. adb shell killall audioserver(or trigger any stream teardown that surfaces as a non-ErrorDisconnectedterminal error).- Call
ctx.resume()on the next play. It rejects, and keeps rejecting forever.
Suggested fix
In AudioContext::resume(), when the driver is initialized but audioPlayer_->resume() fails, rebuild the stream instead of falling into the tryStartDriver() dead end. We are running this patch in production via patch-package:
if (isInitialized_.load(std::memory_order_acquire) && audioPlayer_->resume()) {
setState(ContextState::RUNNING);
return true;
}
#ifdef ANDROID
// An initialized driver whose stream can no longer start (audioserver
// restart, route change missed by onErrorAfterClose's ErrorDisconnected-only
// recovery) previously wedged forever: resume() failed and tryStartDriver()
// early-returned false on isInitialized_. Rebuild the stream so the next
// resume() heals the context instead.
if (isInitialized_.load(std::memory_order_acquire)) {
audioPlayer_->cleanup();
if (!audioPlayer_->openAudioStream()) {
return false;
}
isInitialized_.store(false, std::memory_order_release);
}
#endif
return tryStartDriver();
}
This makes resume() self-healing: if the rebuild fails (audio HW genuinely unavailable at that moment), the next resume() retries it. Widening onErrorAfterClose to recover from all terminal errors would also help, but the resume()-side rebuild covers the failed-reopen case too, and it puts recovery on a code path the app is already calling whenever audio is needed.
Happy to open a PR with the change if you'd take it.
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 common/cpp/audioapi/core/AudioContext.cpp, then inspect android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp and the existing resume and stream-recovery paths. Reproduce with the documented audioserver kill if possible, and verify that a failed stream resume rebuilds the stream so a later resume can recover rather than failing permanently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, cpp, react-native
- Domain
- audio-video-rtc, mobile-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100