From f15c40aadec0b005e90ff06ac776d5524efd1de8 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Mon, 24 Aug 2026 16:11:29 +0200 Subject: [PATCH 01/16] fix(AudioPlayer): modify AudioPlayer::resume, expand AudioPlayer::onErrorAfterClose --- .../cpp/audioapi/android/core/AudioPlayer.cpp | 37 +++++++++++++++---- .../cpp/audioapi/android/core/AudioPlayer.h | 2 + 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 38371018f..9ddbc8557 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -88,11 +88,20 @@ bool AudioPlayer::resume() { } if (mStream_ != nullptr) { - auto result = mStream_->requestStart() == oboe::Result::OK; - isRunning_.store(result, std::memory_order_release); - return result; + if (mStream_->requestStart() == oboe::Result::OK) { + isRunning_.store(true, std::memory_order_release); + return true; + } } + if (rebuildStream()) { + if (mStream_ != nullptr && mStream_->requestStart() == oboe::Result::OK) { + isRunning_.store(true, std::memory_order_release); + return true; + } + } + + isRunning_.store(false, std::memory_order_release); return false; } @@ -158,7 +167,8 @@ AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numF } void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) { - if (error != oboe::Result::ErrorDisconnected || driverMutex_ == nullptr) { + // error != oboe::Result::ErrorDisconnected condition is deleted to handle more cases of errors + if (driverMutex_ == nullptr) { return; } @@ -176,9 +186,17 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro return; } - cleanup(); - if (openAudioStream()) { - resume(); + // Check if the stream was expected to be running when the error occurred + const bool wasRunning = isRunning_.load(std::memory_order_acquire); + + if (!rebuildStream()) { + return; + } + + // Restart the stream if it was expected to be running when the error occurred + if (wasRunning) { + auto result = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(result, std::memory_order_release); } } @@ -225,4 +243,9 @@ double AudioPlayer::getOutputLatency() const { return minBaseLatency; } + +bool AudioPlayer::rebuildStream() { + cleanup(); + return openAudioStream(); +} } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 2f9a4ac01..760fd5c7f 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -69,6 +69,8 @@ class AudioPlayer : public CommonPlayer, std::weak_ptr context_; bool openAudioStream(); + + bool rebuildStream(); }; } // namespace audioapi From 88e61675d8c5cfc288cfef44b905057abbb200a8 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Wed, 26 Aug 2026 15:54:02 +0200 Subject: [PATCH 02/16] fix(AudioPlayer): update onErrorAfterClose, delete stream-rebuilding logic from resume --- .../cpp/audioapi/android/core/AudioPlayer.cpp | 70 +++++++++++-------- .../cpp/audioapi/android/core/AudioPlayer.h | 1 - .../common/cpp/audioapi/core/AudioContext.cpp | 2 + 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 9ddbc8557..595b312db 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -55,21 +55,38 @@ bool AudioPlayer::openAudioStream() { return true; } +bool AudioPlayer::rebuildStream() { + cleanup(); + return openAudioStream(); +} + bool AudioPlayer::start() { std::scoped_lock lock(streamMutex_); - if (!isInitialized_.load(std::memory_order_acquire)) { + + // Ensure a live stream exists. A prior Oboe death can leave isInitialized_ true + // with a closed/null stream, or a stream that rejects requestStart. + if (!isInitialized_.load(std::memory_order_acquire) || mStream_ == nullptr) { + if (isInitialized_.load(std::memory_order_acquire) || mStream_ != nullptr) { + cleanup(); + } if (!openAudioStream()) { return false; } } - if (mStream_ != nullptr) { - auto result = mStream_->requestStart() == oboe::Result::OK; - isRunning_.store(result, std::memory_order_release); - return result; + if (mStream_ != nullptr && mStream_->requestStart() == oboe::Result::OK) { + isRunning_.store(true, std::memory_order_release); + return true; } - return false; + if (!rebuildStream() || mStream_ == nullptr) { + isRunning_.store(false, std::memory_order_release); + return false; + } + + const bool started = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(started, std::memory_order_release); + return started; } void AudioPlayer::stop() { @@ -88,20 +105,11 @@ bool AudioPlayer::resume() { } if (mStream_ != nullptr) { - if (mStream_->requestStart() == oboe::Result::OK) { - isRunning_.store(true, std::memory_order_release); - return true; - } - } - - if (rebuildStream()) { - if (mStream_ != nullptr && mStream_->requestStart() == oboe::Result::OK) { - isRunning_.store(true, std::memory_order_release); - return true; - } + auto result = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(result, std::memory_order_release); + return result; } - isRunning_.store(false, std::memory_order_release); return false; } @@ -167,11 +175,19 @@ AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numF } void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) { - // error != oboe::Result::ErrorDisconnected condition is deleted to handle more cases of errors if (driverMutex_ == nullptr) { return; } + switch (error) { + case oboe::Result::ErrorDisconnected: + case oboe::Result::ErrorTimeout: + case oboe::Result::ErrorInternal: + break; + default: + return; + } + // Serialize with start()/resume()/suspend()/close() on the JS / promise-pool threads. std::scoped_lock lock(*driverMutex_, streamMutex_); @@ -186,17 +202,18 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro return; } - // Check if the stream was expected to be running when the error occurred + // Capture before rebuild: cleanup clears the stream that was expected to run. const bool wasRunning = isRunning_.load(std::memory_order_acquire); if (!rebuildStream()) { + isRunning_.store(false, std::memory_order_release); return; } - // Restart the stream if it was expected to be running when the error occurred - if (wasRunning) { - auto result = mStream_->requestStart() == oboe::Result::OK; - isRunning_.store(result, std::memory_order_release); + // requestStart only — do not call resume() (keeps resume free of rebuild side effects). + if (wasRunning && mStream_ != nullptr) { + const bool started = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(started, std::memory_order_release); } } @@ -243,9 +260,4 @@ double AudioPlayer::getOutputLatency() const { return minBaseLatency; } - -bool AudioPlayer::rebuildStream() { - cleanup(); - return openAudioStream(); -} } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 760fd5c7f..107ec9236 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -69,7 +69,6 @@ class AudioPlayer : public CommonPlayer, std::weak_ptr context_; bool openAudioStream(); - bool rebuildStream(); }; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index c7a038e48..113d48ca4 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -116,6 +116,8 @@ bool AudioContext::resume(const std::shared_ptr> &p getGraph()->disableProducerSelfDrain(); if (audioPlayer_->resume()) { result = true; + } else if (audioPlayer_->rebuildStream() && audioPlayer_->resume()) { + result = true; } else { getGraph()->enableProducerSelfDrain(); } From 194e3bc59a9667cfe40f3f025f03b2a93f253d94 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Wed, 26 Aug 2026 15:54:02 +0200 Subject: [PATCH 03/16] fix(AudioPlayer): update onErrorAfterClose, remove rebuilding the stream from resume --- .../cpp/audioapi/android/core/AudioPlayer.cpp | 66 +++++++++++-------- .../cpp/audioapi/android/core/AudioPlayer.h | 1 - 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 9ddbc8557..47ea5c8fe 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -55,21 +55,38 @@ bool AudioPlayer::openAudioStream() { return true; } +bool AudioPlayer::rebuildStream() { + cleanup(); + return openAudioStream(); +} + bool AudioPlayer::start() { std::scoped_lock lock(streamMutex_); - if (!isInitialized_.load(std::memory_order_acquire)) { + + // Ensure a live stream exists. A prior Oboe death can leave isInitialized_ true + // with a closed/null stream, or a stream that rejects requestStart. + if (!isInitialized_.load(std::memory_order_acquire) || mStream_ == nullptr) { + if (isInitialized_.load(std::memory_order_acquire) || mStream_ != nullptr) { + cleanup(); + } if (!openAudioStream()) { return false; } } - if (mStream_ != nullptr) { - auto result = mStream_->requestStart() == oboe::Result::OK; - isRunning_.store(result, std::memory_order_release); - return result; + if (mStream_ != nullptr && mStream_->requestStart() == oboe::Result::OK) { + isRunning_.store(true, std::memory_order_release); + return true; } - return false; + if (!rebuildStream() || mStream_ == nullptr) { + isRunning_.store(false, std::memory_order_release); + return false; + } + + const bool started = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(started, std::memory_order_release); + return started; } void AudioPlayer::stop() { @@ -88,20 +105,11 @@ bool AudioPlayer::resume() { } if (mStream_ != nullptr) { - if (mStream_->requestStart() == oboe::Result::OK) { - isRunning_.store(true, std::memory_order_release); - return true; - } - } - - if (rebuildStream()) { - if (mStream_ != nullptr && mStream_->requestStart() == oboe::Result::OK) { - isRunning_.store(true, std::memory_order_release); - return true; - } + auto result = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(result, std::memory_order_release); + return result; } - isRunning_.store(false, std::memory_order_release); return false; } @@ -167,11 +175,19 @@ AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numF } void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) { - // error != oboe::Result::ErrorDisconnected condition is deleted to handle more cases of errors if (driverMutex_ == nullptr) { return; } + switch (error) { + case oboe::Result::ErrorDisconnected: + case oboe::Result::ErrorTimeout: + case oboe::Result::ErrorInternal: + break; + default: + return; + } + // Serialize with start()/resume()/suspend()/close() on the JS / promise-pool threads. std::scoped_lock lock(*driverMutex_, streamMutex_); @@ -190,13 +206,14 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro const bool wasRunning = isRunning_.load(std::memory_order_acquire); if (!rebuildStream()) { + isRunning_.store(false, std::memory_order_release); return; } // Restart the stream if it was expected to be running when the error occurred - if (wasRunning) { - auto result = mStream_->requestStart() == oboe::Result::OK; - isRunning_.store(result, std::memory_order_release); + if (wasRunning && mStream_ != nullptr) { + const bool started = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(started, std::memory_order_release); } } @@ -243,9 +260,4 @@ double AudioPlayer::getOutputLatency() const { return minBaseLatency; } - -bool AudioPlayer::rebuildStream() { - cleanup(); - return openAudioStream(); -} } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 760fd5c7f..107ec9236 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -69,7 +69,6 @@ class AudioPlayer : public CommonPlayer, std::weak_ptr context_; bool openAudioStream(); - bool rebuildStream(); }; From 995dca8b3990132b785765489e010cf485971591 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Mon, 14 Sep 2026 14:58:07 +0200 Subject: [PATCH 04/16] fix: added onerror event handler to AudioContext --- .../cpp/audioapi/android/core/AudioPlayer.cpp | 10 ++++++++ .../HostObjects/AudioContextHostObject.cpp | 16 ++++++++++++ .../HostObjects/AudioContextHostObject.h | 1 + .../common/cpp/audioapi/core/AudioContext.cpp | 25 +++++++++++++++++++ .../common/cpp/audioapi/core/AudioContext.h | 9 +++++++ .../src/core/AudioContext.ts | 16 ++++++++++++ .../src/jsi-interfaces.ts | 2 ++ .../react-native-audio-api/src/mock/index.ts | 2 ++ .../src/web-core/AudioContext.web.ts | 16 ++++++++++++ 9 files changed, 97 insertions(+) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 7241c2e9f..afc1a4b63 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -61,6 +61,7 @@ bool AudioPlayer::rebuildStream() { } bool AudioPlayer::start() { + __android_log_print(ANDROID_LOG_ERROR, "AudioPlayer", "=== TEST LOGOWANIA - C++ DZIALA ==="); std::scoped_lock lock(streamMutex_); if (!isInitialized_.load(std::memory_order_acquire)) { if (!openAudioStream()) { @@ -71,6 +72,7 @@ bool AudioPlayer::start() { if (mStream_ != nullptr) { auto result = mStream_->requestStart() == oboe::Result::OK; isRunning_.store(result, std::memory_order_release); + // rebuiltNeeded_ = !result; return result; } @@ -95,6 +97,7 @@ bool AudioPlayer::resume() { if (mStream_ != nullptr) { auto result = mStream_->requestStart() == oboe::Result::OK; isRunning_.store(result, std::memory_order_release); + // rebuiltNeeded_ = !result; return result; } @@ -166,11 +169,14 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro if (driverMutex_ == nullptr) { return; } + __android_log_print(ANDROID_LOG_ERROR, "AudioPlayer", "onErrorAfterClose"); + __android_log_print(ANDROID_LOG_ERROR, "AudioPlayer", "error: %s", oboe::convertToText(error)); switch (error) { case oboe::Result::ErrorDisconnected: case oboe::Result::ErrorTimeout: case oboe::Result::ErrorInternal: + case oboe::Result::ErrorNoService: break; default: return; @@ -190,11 +196,15 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro return; } + context->onStreamFail(); + return; + // Check if the stream was expected to be running when the error occurred const bool wasRunning = isRunning_.load(std::memory_order_acquire); if (!rebuildStream()) { isRunning_.store(false, std::memory_order_release); + context->onStreamFail(); return; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp index 4bc1c6843..6f280d210 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp @@ -20,6 +20,7 @@ AudioContextHostObject::AudioContextHostObject( callInvoker) { addGetters(JSI_EXPORT_PROPERTY_GETTER(AudioContextHostObject, outputLatency)); addGetters(JSI_EXPORT_PROPERTY_GETTER(AudioContextHostObject, baseLatency)); + addSetters(JSI_EXPORT_PROPERTY_SETTER(AudioContextHostObject, onerror)); addFunctions( JSI_EXPORT_FUNCTION(AudioContextHostObject, close), JSI_EXPORT_FUNCTION(AudioContextHostObject, resume), @@ -78,4 +79,19 @@ JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, createMediaElementSource) { return object; } +JSI_PROPERTY_SETTER_IMPL(AudioContextHostObject, onerror) { + auto audioContext = std::static_pointer_cast(context_); + + if (!value.isObject() || !value.getObject(runtime).isFunction(runtime)) { + audioContext->setOnError(nullptr); + return; + } + + auto jsFunc = std::make_shared(value.getObject(runtime).getFunction(runtime)); + + audioContext->setOnError([jsFunc, invoker = callInvoker_, rt = &runtime]() { + invoker->invokeAsync([jsFunc, rt]() { jsFunc->call(*rt); }); + }); +} + } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h index 4952c6c85..3761f7111 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h @@ -26,5 +26,6 @@ class AudioContextHostObject : public BaseAudioContextHostObject { JSI_PROPERTY_GETTER_DECL(outputLatency); JSI_PROPERTY_GETTER_DECL(baseLatency); + JSI_PROPERTY_SETTER_DECL(onerror); }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index c7a038e48..d76bd4e9f 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -8,9 +8,15 @@ #include #include + +#ifdef ANDROID +#include +#endif + #include #include #include +#include namespace audioapi { AudioContext::AudioContext( @@ -121,6 +127,7 @@ bool AudioContext::resume(const std::shared_ptr> &p } } else { result = tryStartDriver(); + __android_log_print(ANDROID_LOG_ERROR, "AudioContext", "tryStartDriver"); } if (result) { @@ -190,4 +197,22 @@ double AudioContext::getOutputLatency() const { return audioPlayer_->getOutputLatency(); } +void AudioContext::setOnError(std::function callback) { + onerror = std::move(callback); +} + +void AudioContext::onStreamFail() { + assertDriverMutexHeld(); + + if (audioPlayer_ != nullptr) { + audioPlayer_->cleanup(); + } + + isInitialized_.store(false, std::memory_order_release); + + if (onerror) { + onerror(); + } +} + } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h index 0cad163a2..f8497ae82 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h @@ -7,6 +7,7 @@ #include #include +#include #include namespace audioapi { @@ -37,6 +38,12 @@ class AudioContext : public BaseAudioContext { /// @returns The output latency in seconds. [[nodiscard]] double getOutputLatency() const; + /// @brief Called when the audio stream failed to rebuild. + /// @note This method is called when the audio stream fails. + void onStreamFail(); + + void setOnError(std::function callback); + private: std::shared_ptr audioPlayer_; std::atomic isInitialized_{false}; @@ -51,6 +58,8 @@ class AudioContext : public BaseAudioContext { /// Blocks until no audio I/O callback is in flight. Caller must hold `driverMutex_`. void waitForRenderQuiescence() const; + + std::function onerror; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index 8941e656c..2fc98e11c 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -8,6 +8,8 @@ import BaseAudioContext from './BaseAudioContext'; import MediaElementAudioSourceNode from './MediaElementAudioSourceNode'; export default class AudioContext extends BaseAudioContext { + private onerrorCallback: (() => void) | null = null; + constructor(options?: AudioContextOptions) { if (options?.sampleRate != null) { assertSupportedSampleRate(options.sampleRate); @@ -28,6 +30,20 @@ export default class AudioContext extends BaseAudioContext { return (this.context as IAudioContext).outputLatency; } + /** + * Web Audio `onerror` — fired when the output device/stream fails natively. + * + * @see https://webaudio.github.io/web-audio-api/#dom-audiocontext-onerror + */ + public get onerror(): (() => void) | null { + return this.onerrorCallback; + } + + public set onerror(callback: (() => void) | null) { + this.onerrorCallback = callback; + (this.context as IAudioContext).onerror = callback; + } + async close(): Promise { if (this._state === 'closed') { throw new InvalidStateError('Cannot close a closed audio context.'); diff --git a/packages/react-native-audio-api/src/jsi-interfaces.ts b/packages/react-native-audio-api/src/jsi-interfaces.ts index 9a9a2a552..4daafe78e 100644 --- a/packages/react-native-audio-api/src/jsi-interfaces.ts +++ b/packages/react-native-audio-api/src/jsi-interfaces.ts @@ -91,6 +91,8 @@ export interface IAudioContext extends IBaseAudioContext { close(): Promise; resume(): Promise; suspend(): Promise; + /** Web Audio `onerror` — JS function stored on the native HostObject. */ + onerror: (() => void) | null; } export interface IOfflineAudioContext extends IBaseAudioContext { diff --git a/packages/react-native-audio-api/src/mock/index.ts b/packages/react-native-audio-api/src/mock/index.ts index 5d596e392..724057f58 100644 --- a/packages/react-native-audio-api/src/mock/index.ts +++ b/packages/react-native-audio-api/src/mock/index.ts @@ -825,6 +825,8 @@ class AudioContextMock extends BaseAudioContextMock { return Promise.resolve(); } + onerror: (() => void) | null = null; + createMediaElementSource( mediaElement: HTMLMediaElement | AudioNodeMock ): MediaElementAudioSourceNodeMock { diff --git a/packages/react-native-audio-api/src/web-core/AudioContext.web.ts b/packages/react-native-audio-api/src/web-core/AudioContext.web.ts index 220b4a85d..308f5d378 100644 --- a/packages/react-native-audio-api/src/web-core/AudioContext.web.ts +++ b/packages/react-native-audio-api/src/web-core/AudioContext.web.ts @@ -193,4 +193,20 @@ export default class AudioContext implements BaseAudioContext { async suspend(): Promise { await this.context.suspend(); } + + private _onerrorCallback: + | ((this: globalThis.AudioContext, ev: Event) => unknown) + | null = null; + + public get onerror(): + | ((this: globalThis.AudioContext, ev: Event) => unknown) + | null { + return this._onerrorCallback; + } + + public set onerror( + callback: ((this: globalThis.AudioContext, ev: Event) => unknown) | null + ) { + this._onerrorCallback = callback; + } } From 890fa567e9efb26dd95db2b5a1e67ce096f1e695 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Mon, 14 Sep 2026 15:06:43 +0200 Subject: [PATCH 05/16] chore: deleted logs --- .../src/main/cpp/audioapi/android/core/AudioPlayer.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index afc1a4b63..2d54a93f5 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -61,7 +61,6 @@ bool AudioPlayer::rebuildStream() { } bool AudioPlayer::start() { - __android_log_print(ANDROID_LOG_ERROR, "AudioPlayer", "=== TEST LOGOWANIA - C++ DZIALA ==="); std::scoped_lock lock(streamMutex_); if (!isInitialized_.load(std::memory_order_acquire)) { if (!openAudioStream()) { @@ -72,7 +71,6 @@ bool AudioPlayer::start() { if (mStream_ != nullptr) { auto result = mStream_->requestStart() == oboe::Result::OK; isRunning_.store(result, std::memory_order_release); - // rebuiltNeeded_ = !result; return result; } @@ -97,7 +95,6 @@ bool AudioPlayer::resume() { if (mStream_ != nullptr) { auto result = mStream_->requestStart() == oboe::Result::OK; isRunning_.store(result, std::memory_order_release); - // rebuiltNeeded_ = !result; return result; } @@ -169,8 +166,6 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro if (driverMutex_ == nullptr) { return; } - __android_log_print(ANDROID_LOG_ERROR, "AudioPlayer", "onErrorAfterClose"); - __android_log_print(ANDROID_LOG_ERROR, "AudioPlayer", "error: %s", oboe::convertToText(error)); switch (error) { case oboe::Result::ErrorDisconnected: @@ -196,9 +191,6 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro return; } - context->onStreamFail(); - return; - // Check if the stream was expected to be running when the error occurred const bool wasRunning = isRunning_.load(std::memory_order_acquire); From e991d8fbb639ce1a8b8b324aed700cd14d4c9eda Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Mon, 14 Sep 2026 16:34:01 +0200 Subject: [PATCH 06/16] fix: add reentrancy guard and deleted isFailed_ --- .../cpp/audioapi/android/core/AudioPlayer.cpp | 24 +++++++++++++++++++ .../cpp/audioapi/android/core/AudioPlayer.h | 2 ++ 2 files changed, 26 insertions(+) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 2d54a93f5..143e7abf3 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -62,6 +62,7 @@ bool AudioPlayer::rebuildStream() { bool AudioPlayer::start() { std::scoped_lock lock(streamMutex_); + if (!isInitialized_.load(std::memory_order_acquire)) { if (!openAudioStream()) { return false; @@ -162,6 +163,18 @@ AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numF return DataCallbackResult::Continue; } +namespace { +struct ReentrancyGuard { + bool &flag; + explicit ReentrancyGuard(bool &f) : flag(f) { + flag = true; + } + ~ReentrancyGuard() { + flag = false; + } +}; +} // namespace + void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) { if (driverMutex_ == nullptr) { return; @@ -177,6 +190,13 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro return; } + // Reentrancy guard - prevent recursive calls to onErrorAfterClose. + static thread_local bool isInsideOnError = false; + if (isInsideOnError) { + return; + } + ReentrancyGuard guard(isInsideOnError); + // Serialize with start()/resume()/suspend()/close() on the JS / promise-pool threads. std::scoped_lock lock(*driverMutex_, streamMutex_); @@ -194,6 +214,7 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro // Check if the stream was expected to be running when the error occurred const bool wasRunning = isRunning_.load(std::memory_order_acquire); + // Best effort rebuild - only once, then fire AudioContext::onStreamFail. if (!rebuildStream()) { isRunning_.store(false, std::memory_order_release); context->onStreamFail(); @@ -204,6 +225,9 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro if (wasRunning && mStream_ != nullptr) { const bool started = mStream_->requestStart() == oboe::Result::OK; isRunning_.store(started, std::memory_order_release); + if (!started) { + context->onStreamFail(); + } } } diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 107ec9236..36ae4685e 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -70,6 +70,8 @@ class AudioPlayer : public CommonPlayer, bool openAudioStream(); bool rebuildStream(); + + std::atomic isFailed_{false}; }; } // namespace audioapi From d603c6ed35cdddb3e1fad047d5cf1ddb5178aad3 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Mon, 14 Sep 2026 17:10:56 +0200 Subject: [PATCH 07/16] chore: delete log from AudioContext --- .../common/cpp/audioapi/core/AudioContext.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index d76bd4e9f..a2126cdf8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -127,7 +127,6 @@ bool AudioContext::resume(const std::shared_ptr> &p } } else { result = tryStartDriver(); - __android_log_print(ANDROID_LOG_ERROR, "AudioContext", "tryStartDriver"); } if (result) { From d024c14acc9486d9278e6cbc42151936f1d2baf4 Mon Sep 17 00:00:00 2001 From: Barbara Wojtarowicz Date: Wed, 16 Sep 2026 13:19:06 +0200 Subject: [PATCH 08/16] fix: add EventCaller to AudioContext to handle onerror --- .../cpp/audioapi/android/core/AudioPlayer.h | 2 -- .../swmansion/audioapi/system/AudioEvent.kt | 1 + .../HostObjects/AudioContextHostObject.cpp | 16 ++++------- .../HostObjects/AudioContextHostObject.h | 1 + .../HostObjects/utils/JsEnumParser.cpp | 2 ++ .../common/cpp/audioapi/core/AudioContext.cpp | 18 ++++-------- .../common/cpp/audioapi/core/AudioContext.h | 9 +++--- .../common/cpp/audioapi/events/AudioEvent.h | 1 + .../events/AudioEventPayloadMapping.hpp | 1 + .../cpp/test/src/events/EventCallerTest.cpp | 1 + .../src/core/AudioContext.ts | 28 +++++++++---------- .../src/events/types.ts | 1 + .../src/jsi-interfaces.ts | 3 +- 13 files changed, 38 insertions(+), 46 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 36ae4685e..107ec9236 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -70,8 +70,6 @@ class AudioPlayer : public CommonPlayer, bool openAudioStream(); bool rebuildStream(); - - std::atomic isFailed_{false}; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt index 324fac4e2..a60d76998 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt @@ -27,4 +27,5 @@ enum class AudioEvent { RECORDER_ERROR, BUFFERING_STATE_CHANGE, STATE_CHANGE, + CONTEXT_ERROR, } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp index 6f280d210..65917a3e5 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp @@ -28,6 +28,10 @@ AudioContextHostObject::AudioContextHostObject( JSI_EXPORT_FUNCTION(AudioContextHostObject, createMediaElementSource)); } +AudioContextHostObject::~AudioContextHostObject() { + std::static_pointer_cast(context_)->assignOnErrorCallbackId(0); +} + JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, close) { return promiseVendor_->createPromise([this](Promise &&promise) { auto contextPromise = ContextPromiseResolver::makeContextPromiseResolver( @@ -81,17 +85,7 @@ JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, createMediaElementSource) { JSI_PROPERTY_SETTER_IMPL(AudioContextHostObject, onerror) { auto audioContext = std::static_pointer_cast(context_); - - if (!value.isObject() || !value.getObject(runtime).isFunction(runtime)) { - audioContext->setOnError(nullptr); - return; - } - - auto jsFunc = std::make_shared(value.getObject(runtime).getFunction(runtime)); - - audioContext->setOnError([jsFunc, invoker = callInvoker_, rt = &runtime]() { - invoker->invokeAsync([jsFunc, rt]() { jsFunc->call(*rt); }); - }); + audioContext->assignOnErrorCallbackId(std::stoull(value.getString(runtime).utf8(runtime))); } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h index 3761f7111..470bf2a35 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h @@ -18,6 +18,7 @@ class AudioContextHostObject : public BaseAudioContextHostObject { const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker); + ~AudioContextHostObject() override; JSI_HOST_FUNCTION_DECL(close); JSI_HOST_FUNCTION_DECL(resume); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp index 0e481a80b..1eaa8b8da 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp @@ -152,6 +152,8 @@ AudioEvent audioEventFromString(const std::string &event) { return AudioEvent::BUFFERING_STATE_CHANGE; if (event == "stateChange") return AudioEvent::STATE_CHANGE; + if (event == "contextError") + return AudioEvent::CONTEXT_ERROR; throw std::invalid_argument("Unknown audio event: " + event); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index 03e7f18e2..b3d5ac4a3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -9,20 +9,16 @@ #include #include -#ifdef ANDROID -#include -#endif - #include -#include #include -#include namespace audioapi { AudioContext::AudioContext( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry) - : BaseAudioContext(sampleRate, audioEventHandlerRegistry), isInitialized_(false) { + : BaseAudioContext(sampleRate, audioEventHandlerRegistry), + isInitialized_(false), + onErrorEvent_(audioEventHandlerRegistry) { // Context starts SUSPENDED with no audio-thread consumer. Let the producer // drain the channels itself until start()/resume() hands draining to the // audio callback (same pattern as OfflineAudioContext before rendering). @@ -194,8 +190,8 @@ double AudioContext::getOutputLatency() const { return audioPlayer_->getOutputLatency(); } -void AudioContext::setOnError(std::function callback) { - onerror = std::move(callback); +void AudioContext::assignOnErrorCallbackId(uint64_t callbackId) { + onErrorEvent_.assignCallbackId(callbackId); } void AudioContext::onStreamFail() { @@ -207,9 +203,7 @@ void AudioContext::onStreamFail() { isInitialized_.store(false, std::memory_order_release); - if (onerror) { - onerror(); - } + onErrorEvent_.dispatchEmpty(); } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h index f8497ae82..16b8efcc6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h @@ -2,12 +2,13 @@ #include #include +#include +#include #include #include #include #include -#include #include namespace audioapi { @@ -42,7 +43,7 @@ class AudioContext : public BaseAudioContext { /// @note This method is called when the audio stream fails. void onStreamFail(); - void setOnError(std::function callback); + void assignOnErrorCallbackId(uint64_t callbackId); private: std::shared_ptr audioPlayer_; @@ -51,6 +52,8 @@ class AudioContext : public BaseAudioContext { /// control thread waits on suspend/close. std::atomic currentRenders_{0}; + EventCaller onErrorEvent_; + bool isDriverRunning() const override; /// Caller must hold `driverMutex_`. @@ -58,8 +61,6 @@ class AudioContext : public BaseAudioContext { /// Blocks until no audio I/O callback is in flight. Caller must hold `driverMutex_`. void waitForRenderQuiescence() const; - - std::function onerror; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEvent.h b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEvent.h index beb125403..6042480fd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEvent.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEvent.h @@ -30,5 +30,6 @@ enum class AudioEvent : uint8_t { RECORDER_ERROR, BUFFERING_STATE_CHANGE, STATE_CHANGE, + CONTEXT_ERROR, }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventPayloadMapping.hpp b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventPayloadMapping.hpp index eb44b720c..9385e22c6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventPayloadMapping.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventPayloadMapping.hpp @@ -42,6 +42,7 @@ AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::BUFFER_ENDED, BufferEndedPayload); AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::RECORDER_ERROR, StringPayload); AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::BUFFERING_STATE_CHANGE, BoolValuePayload); AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::STATE_CHANGE, StringPayload); +AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::CONTEXT_ERROR, EmptyPayload); #undef AUDIOAPI_DEFINE_EVENT_PAYLOAD diff --git a/packages/react-native-audio-api/common/cpp/test/src/events/EventCallerTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/events/EventCallerTest.cpp index da36dede5..28a282b55 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/events/EventCallerTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/events/EventCallerTest.cpp @@ -15,6 +15,7 @@ constexpr uint64_t ERROR_CALLBACK_ID = 88; constexpr uint64_t POSITION_CALLBACK_ID = 19; static_assert(EventPayloadFor); +static_assert(EventPayloadFor); static_assert(EventPayloadFor); static_assert(EventPayloadFor); static_assert(EventPayloadFor); diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index 9dd416b66..8779bae01 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -1,6 +1,7 @@ import { InvalidStateError } from '../errors'; import { assertSupportedSampleRate } from '../utils/validation'; import { AudioTagHandle } from '../Audio/types'; +import { AudioEventEmitter } from '../events'; import { IAudioContext } from '../jsi-interfaces'; import AudioManager from '../system'; import { AudioContextOptions, ContextState } from '../types'; @@ -8,7 +9,11 @@ import BaseAudioContext from './BaseAudioContext'; import MediaElementAudioSourceNode from './MediaElementAudioSourceNode'; export default class AudioContext extends BaseAudioContext { - private onerrorCallback: (() => void) | null = null; + public onerror: (() => void) | null = null; + + private readonly errorSubscription: ReturnType< + AudioEventEmitter['addAudioEventListener'] + >; constructor(options?: AudioContextOptions) { if (options?.sampleRate != null) { @@ -20,6 +25,13 @@ export default class AudioContext extends BaseAudioContext { options?.sampleRate || AudioManager.getDevicePreferredSampleRate() ) ); + + this.errorSubscription = this.audioEventEmitter.addAudioEventListener( + 'contextError', + () => this.onerror?.() + ); + (this.context as IAudioContext).onerror = + this.errorSubscription.subscriptionId; } public get baseLatency(): number { @@ -30,20 +42,6 @@ export default class AudioContext extends BaseAudioContext { return (this.context as IAudioContext).outputLatency; } - /** - * Web Audio `onerror` — fired when the output device/stream fails natively. - * - * @see https://webaudio.github.io/web-audio-api/#dom-audiocontext-onerror - */ - public get onerror(): (() => void) | null { - return this.onerrorCallback; - } - - public set onerror(callback: (() => void) | null) { - this.onerrorCallback = callback; - (this.context as IAudioContext).onerror = callback; - } - async close(): Promise { if (this._state === 'closed') { throw new InvalidStateError('Cannot close a closed audio context.'); diff --git a/packages/react-native-audio-api/src/events/types.ts b/packages/react-native-audio-api/src/events/types.ts index c025a1aef..c3114e931 100644 --- a/packages/react-native-audio-api/src/events/types.ts +++ b/packages/react-native-audio-api/src/events/types.ts @@ -82,6 +82,7 @@ interface AudioAPIEvents { /** `value` is true while an `