Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ - (NSString *)attachSourceNodeToAudioEngine {
return [self.audioEngine
attachSourceNodeWithRenderBlock:[self testSourceRenderBlock]
sampleRate:44100
channelCount:2];
channelCount:2
onOutputRecoveryFailed:nil];
}

- (void)testCleanupDestroysInternalEngineAndResetsStateAndDeactivatesSession {
Expand Down Expand Up @@ -370,7 +371,8 @@ - (void)testAttachSourceNodeStoresAndConnectsSource {
NSString *sourceNodeId = [self.audioEngine
attachSourceNodeWithRenderBlock:[self testSourceRenderBlock]
sampleRate:44100
channelCount:2];
channelCount:2
onOutputRecoveryFailed:nil];
AVAudioSourceNode *sourceNode = self.audioEngine.sourceNodes[sourceNodeId];
AVAudioFormat *format = self.audioEngine.sourceFormats[sourceNodeId];

Expand Down
18 changes: 16 additions & 2 deletions apps/fabric-example/ios/FabricExampleTests/AudioPlayerTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,25 @@
#include <cstddef>
#include <functional>
#include <memory>
#include <mutex>
#include <utility>
#include <vector>

using namespace audioapi;

namespace audioapi {

class AudioContext;

class IOSAudioPlayer : public CommonPlayer {
public:
IOSAudioPlayer(
const std::function<void(DSPAudioBuffer *, int)> &renderAudio,
float sampleRate,
int channelCount,
std::atomic<uint32_t> &currentRenders);
std::atomic<uint32_t> &currentRenders,
std::weak_ptr<AudioContext> context,
std::mutex *driverMutex);
~IOSAudioPlayer() override;

bool start() override;
Expand All @@ -50,6 +55,8 @@
std::atomic<bool> flushOverflowNextPull_;
int pendingSavedCount_;
DSPAudioBuffer pendingSaved_;
std::weak_ptr<AudioContext> context_;
std::mutex *driverMutex_;
};

} // namespace audioapi
Expand Down Expand Up @@ -159,6 +166,7 @@ - (void)stopIfPossible
- (NSString *)attachSourceNodeWithRenderBlock:(AVAudioSourceNodeRenderBlock)renderBlock
sampleRate:(float)sampleRate
channelCount:(AVAudioChannelCount)channelCount
onOutputRecoveryFailed:(OnOutputRecoveryFailedBlock)onOutputRecoveryFailed
{
self.attachSourceNodeCallCount += 1;
self.lastAttachedRenderBlock = renderBlock;
Expand Down Expand Up @@ -235,7 +243,13 @@ - (void)cleanup
float sampleRate,
int channelCount)
: currentRendersStorage_(0),
IOSAudioPlayer(renderAudio, sampleRate, channelCount, currentRendersStorage_) {}
IOSAudioPlayer(
renderAudio,
sampleRate,
channelCount,
currentRendersStorage_,
std::weak_ptr<AudioContext>{},
nullptr) {}

NativeAudioPlayer *replaceAudioPlayer(NativeAudioPlayer *audioPlayer) {
NativeAudioPlayer *previous = audioPlayer_;
Expand Down
18 changes: 18 additions & 0 deletions packages/audiodocs/docs/core/audio-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,21 @@ Resumes a previously suspended audio context.

Inherits [`onstatechange`](./base-audio-context.mdx#onstatechange) from [`BaseAudioContext`](./base-audio-context.mdx#events); `close`, `suspend` and `resume` each fire it once their returned promise has resolved.

### `onerror`

Sets (or removes, when `null` is assigned) a callback fired when the live output stream fails natively and cannot keep rendering — for example after the audio device disconnects or the OS audio service dies while the context is running.


Typical recovery is to call [`resume`](./audio-context.mdx#resume) from the handler (or recreate the context). A short delay before `resume` gives the OS audio server time to recover from some errors, e.g. hardware errors, and prevents rapid retry loops.

```tsx
const audioContext = new AudioContext();

audioContext.onerror = () => {
console.error('Audio output failed');
setTimeout(() => {
void audioContext.resume();
}, 2000);
};
```

Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ 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)) {
if (!openAudioStream()) {
return false;
Expand Down Expand Up @@ -157,10 +163,39 @@ 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 (error != oboe::Result::ErrorDisconnected || driverMutex_ == nullptr) {
if (driverMutex_ == nullptr) {
return;
}

switch (error) {
case oboe::Result::ErrorDisconnected:
case oboe::Result::ErrorTimeout:
case oboe::Result::ErrorInternal:
case oboe::Result::ErrorNoService:
break;
default:
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_);
Expand All @@ -176,9 +211,28 @@ 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 (error == oboe::Result::ErrorDisconnected) {
// Best effort rebuild - only once, then fire AudioContext::onStreamFail.
if (!rebuildStream()) {
isRunning_.store(false, std::memory_order_release);
context->onStreamFail();
return;
}

// Restart the stream if it was expected to be running when the error occurred
if (wasRunning && mStream_ != nullptr) {
const bool started = mStream_->requestStart() == oboe::Result::OK;
isRunning_.store(started, std::memory_order_release);
if (!started) {
context->onStreamFail();
}
}
} else {
isRunning_.store(false, std::memory_order_release);
context->onStreamFail();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class AudioPlayer : public CommonPlayer,
std::weak_ptr<AudioContext> context_;

bool openAudioStream();
bool rebuildStream();
};

} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ enum class AudioEvent {
RECORDER_ERROR,
BUFFERING_STATE_CHANGE,
STATE_CHANGE,
CONTEXT_ERROR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ 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),
JSI_EXPORT_FUNCTION(AudioContextHostObject, suspend),
JSI_EXPORT_FUNCTION(AudioContextHostObject, createMediaElementSource));
}

AudioContextHostObject::~AudioContextHostObject() {
std::static_pointer_cast<AudioContext>(context_)->assignOnErrorCallbackId(0);
}

JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, close) {
return promiseVendor_->createPromise([this](Promise &&promise) {
auto contextPromise = ContextPromiseResolver<void>::makeContextPromiseResolver(
Expand Down Expand Up @@ -78,4 +83,9 @@ JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, createMediaElementSource) {
return object;
}

JSI_PROPERTY_SETTER_IMPL(AudioContextHostObject, onerror) {
auto audioContext = std::static_pointer_cast<AudioContext>(context_);
audioContext->assignOnErrorCallbackId(std::stoull(value.getString(runtime).utf8(runtime)));
}

Comment on lines +86 to +90

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even if it looked simple, it can introduce some errors, such as datarace when swaping the function mid oboe's onError, reference cycle due to strong ptr and that it can be destroyed on different thread. Utilize existing AudioEventHandlerRegistry and compare how events are done using it and implement the onerror this way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class AudioContextHostObject : public BaseAudioContextHostObject {
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
jsi::Runtime *runtime,
const std::shared_ptr<react::CallInvoker> &callInvoker);
~AudioContextHostObject() override;

JSI_HOST_FUNCTION_DECL(close);
JSI_HOST_FUNCTION_DECL(resume);
Expand All @@ -26,5 +27,6 @@ class AudioContextHostObject : public BaseAudioContextHostObject {

JSI_PROPERTY_GETTER_DECL(outputLatency);
JSI_PROPERTY_GETTER_DECL(baseLatency);
JSI_PROPERTY_SETTER_DECL(onerror);
};
} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@

#include <audioapi/core/AudioContext.h>
#include <audioapi/core/destinations/AudioDestinationNode.h>

#include <memory>
#include <string>
#include <thread>

namespace audioapi {
AudioContext::AudioContext(
float sampleRate,
const std::shared_ptr<IAudioEventHandlerRegistry> &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).
Expand Down Expand Up @@ -51,7 +53,9 @@ void AudioContext::initialize(const AudioDestinationNode *destination) {
[this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); },
getSampleRate(),
destination_->getChannelCount(),
currentRenders_);
currentRenders_,
std::static_pointer_cast<AudioContext>(shared_from_this()),
&driverMutex_);
#endif
}

Expand Down Expand Up @@ -188,4 +192,20 @@ double AudioContext::getOutputLatency() const {
return audioPlayer_->getOutputLatency();
}

void AudioContext::assignOnErrorCallbackId(uint64_t callbackId) {
onErrorEvent_.assignCallbackId(callbackId);
}

void AudioContext::onStreamFail() {
assertDriverMutexHeld();

if (audioPlayer_ != nullptr) {
audioPlayer_->cleanup();
}

isInitialized_.store(false, std::memory_order_release);

onErrorEvent_.dispatchEmpty();
}

} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <audioapi/core/BaseAudioContext.h>
#include <audioapi/core/CommonPlayer.h>
#include <audioapi/events/AudioEvent.h>
#include <audioapi/events/EventCaller.hpp>
#include <audioapi/jsi/ContextPromiseResolver.hpp>
#include <audioapi/utils/AudioBuffer.hpp>
#include <audioapi/utils/Macros.h>
Expand Down Expand Up @@ -37,13 +39,21 @@ 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 assignOnErrorCallbackId(uint64_t callbackId);

private:
std::shared_ptr<CommonPlayer> audioPlayer_;
std::atomic<bool> isInitialized_{false};
/// Audio I/O callback thread increments around each platform render callback;
/// control thread waits on suspend/close.
std::atomic<uint32_t> currentRenders_{0};

EventCaller<AudioEvent::CONTEXT_ERROR> onErrorEvent_;

bool isDriverRunning() const override;

/// Caller must hold `driverMutex_`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ enum class AudioEvent : uint8_t {
RECORDER_ERROR,
BUFFERING_STATE_CHANGE,
STATE_CHANGE,
CONTEXT_ERROR,
};
} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ constexpr uint64_t ERROR_CALLBACK_ID = 88;
constexpr uint64_t POSITION_CALLBACK_ID = 19;

static_assert(EventPayloadFor<AudioEvent::ENDED, EmptyPayload>);
static_assert(EventPayloadFor<AudioEvent::CONTEXT_ERROR, EmptyPayload>);
static_assert(EventPayloadFor<AudioEvent::POSITION_CHANGED, DoubleValuePayload>);
static_assert(EventPayloadFor<AudioEvent::RECORDER_ERROR, StringPayload>);
static_assert(EventPayloadFor<AudioEvent::BUFFERING_STATE_CHANGE, BoolValuePayload>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ typedef struct objc_object AudioBufferList;
#include <atomic>
#include <cstddef>
#include <functional>
#include <memory>
#include <mutex>

namespace audioapi {

class AudioContext;
Expand All @@ -24,7 +27,9 @@ class IOSAudioPlayer : public CommonPlayer {
const std::function<void(DSPAudioBuffer *, int)> &renderAudio,
float sampleRate,
int channelCount,
std::atomic<uint32_t> &currentRenders);
std::atomic<uint32_t> &currentRenders,
std::weak_ptr<AudioContext> context,
std::mutex *driverMutex);
~IOSAudioPlayer() override;

DELETE_COPY_AND_MOVE(IOSAudioPlayer);
Expand Down Expand Up @@ -60,6 +65,8 @@ class IOSAudioPlayer : public CommonPlayer {
/// Frames valid at the front of each `pendingSaved_[ch]` (0 … RENDER_QUANTUM_SIZE).
int pendingSavedCount_{0};
DSPAudioBuffer pendingSaved_;
std::weak_ptr<AudioContext> context_;
std::mutex *driverMutex_;
};

} // namespace audioapi
Loading
Loading