Skip to content

fix(ios): recording resume after an interruption - #1257

Open
SomePersonFromMars wants to merge 13 commits into
mainfrom
fix/ios-recording-interrupted
Open

fix(ios): recording resume after an interruption#1257
SomePersonFromMars wants to merge 13 commits into
mainfrom
fix/ios-recording-interrupted

Conversation

@SomePersonFromMars

@SomePersonFromMars SomePersonFromMars commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #1220

⚠️ Breaking changes ⚠️

  • iOS - observeAudioInterruptions(true) no longer replaces native recovery. Native onInterruptionBegin / onInterruptionEnd always run. The flag only enables JS interruption events, which matches the existing AudioManager docs. Apps that subscribed because observing used to skip native resume should not own engine recovery for a recorder (in particular, do not call Recorder.pause() on began - that moves the engine to Paused and disables the native retry). Playback can still pause on began; native does not resume players.

Introduced changes

Fixes iOS recording that never came back after an interruption. Updated library policy: the same take resumes when the app is, or becomes, foreground. Background restart after the interrupter leaves is best-effort; iOS can still refuse I/O while the app is backgrounded.

Responds to the issue

§1 onInterruptionEnd never activated the session; failure poisoned Idle; no input-node guard

  • Activate the session before any engine teardown/rebuild (matches startEngine).
  • On activation or startAndReturnError failure, stay Interrupted and return (do not set Idle). A later retry can run.
  • If a recorder is attached but the input sink did not materialize, stay Interrupted and treat it as capture lost (do not look like a successful start with no buffers).
  • Always try to resume a recorder even when shouldResume is false; playback-only still pauses when the OS says not to resume.

§2 Docs vs behavior for observeAudioInterruptions

  • When JS is configured to observeAudioInterruptions, an appropriate handler runs only when AudioEngine actually transitioned to a new state. For example, if the engine was Interrupted, the handler runs on a successful transition to Running.
  • Native recovery now runs independently of whether JS is observing interruptions, in accordance with the documentation.

§3 No-op setActive:false still ran deactivation

  • setAudioSessionActivity(false) only calls session-deactivation handling if the session was actually managed and active before setActive. It no longer transitions Interrupted to Paused, which used to disable onInterruptionEnd.

§3 / footnote - resume() / store Recording on a dead engine

  • resume() still does not have a return type, but it is now guaranteed that isRecording() is true if and only if the audio engine is not in an interrupted state and the recorder is actually running.
  • According to my experiments and research, it is now most likely never necessary to implement a custom retry policy in JS.
    • AudioEngine's onInterruptionEnd recovery after AVAudioSessionInterruptionTypeEnded can still fail. That can happen, for example, due to a race with a higher-priority app or internal iOS privacy policy. In that case we retry once the app returns to the foreground, which is then guaranteed by iOS to succeed.
    • At the same time, I can imagine a situation where an active retry policy might be useful, but I am unable to trigger a scenario in which the first background interrupt recovery fails and the next one succeeds.
      • If an iOS privacy policy was the cause, I suppose consecutive retries would finish with the same error. It is hard to judge, though, because this is not well documented.
      • On the other hand, if a race with another app caused the failure, then once that app finishes its activity it typically announces an interruption-ended notification as well, which lets our app try to recover.
    • If a reproducible scenario is found in which an active retry policy is necessary, it can be considered again.
    • Relatedly, I do not think it is useful for a JS app developer to handle native iOS errors (for the modules affected by this PR) themselves. If there is a use case for that, it can be considered again. Such a feature would require an architectural update, because the codebase is not currently prepared to surface native iOS errors to JS.

Consumer note - mixWithOthers / CannotInterruptOthers

  • Documented: mixable playAndRecord is required to avoid CannotInterruptOthers when activating in the background, and is not sufficient for background restart. The Record demo enables mixWithOthers with default mode.

Additional useful tweaks and fixes

  • Retry on foreground. Once recovery from an interruption fails and does not lead to an audio-engine transition, engine recovery is retried when the app returns to the foreground via WillEnterForegroundNotification and DidBecomeActiveNotification. iOS guarantees that a foreground audio start succeeds.
  • Generalized handleInputConfigurationChange to handle errors. A failed engine resume now finalizes the audio recorder.
  • Player detach on failed startIfNecessary, matching the recorder. A later recorder start must not play leftover source audio.
  • Record demo: observeAudioInterruptions(true); freeze the waveform on began, unfreeze only on ended.

Docs

  • New AudioManager subsection "Resume recording after an interruption".
  • Extended instructions related to iOS audio session configuration.

Manual tests and reproductions used while debugging

This issue is hard to reproduce with automated tests. During research and implementation I used the following manual tests, with the apps/fabric-example app and the included Recorder demo.

  • Start record; go background; interrupt with Siri; stop record
  • Start record; go background; interrupt with Siri; tell it to open FabricExample; stop record
  • Start record; go background; interrupt with meet.jit.si; disconnect; stop record
  • Start record; go background; interrupt with meet.jit.si; disconnect and close the Safari tab; stop record
  • Start record; interrupt with FaceTime; stop record
  • Start record; go background; interrupt with FaceTime; stop record
  • Record a short take and stop; start exclusive audio (could stay in foreground and accept FaceTime); play the recorded take; end the exclusive audio; end the recorded playback before it ends; record again
  • Record a short take and stop; start exclusive audio (could stay in foreground and accept FaceTime); play the recorded take; end the exclusive audio; wait until playback ends; record again
  • Record a short take and stop; start exclusive audio (could stay in foreground and accept FaceTime); play the recorded take and wait until playback reaches the end; end the exclusive audio; end the recorded playback; record again
  • Record a short take and stop; start exclusive audio (could stay in foreground and accept FaceTime); play the recorded take and wait until playback reaches the end; end the exclusive audio; end the recorded playback; record again (do this step quickly enough after ending the exclusive audio)

Safari WebRTC microphone session side note. One of the most mysterious errors I found, while experimenting with a meet.jit.si interruption, is the following:

Error while restarting the audio engine after interruption: Error Domain=com.apple.coreaudio.avfaudio Code=2003329396 "(null)" UserInfo={failed call=err = PerformCommand(*ioNode, kAUStartIO, NULL, 0)}

It is the result of executing [self.audioEngine startAndReturnError:&error] in AudioEngine's onInterruptionEnd. Code 2003329396 corresponds to the 'what' FourCC, which is AVAudioSessionErrorCodeUnspecified. It is not well documented, and I am unable to resume the session in the background with this error even with multiple active delayed retries, until I return to the foreground. I have two hypotheses that explain it: the first is iOS privacy policy; the second is that the interruption modifies the shared iOS audio session settings, after which a recovery to the original ones is impossible in the background.

I mention it because I thought it was a good example for an active retry, but such a policy does not bear fruit.

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added/Conducted relevant tests
  • Performed self-review of the code
  • Updated Web Audio API coverage - N/A (iOS session / engine, not a Web Audio node)
  • Added support for web - N/A
  • Updated old arch android spec file - N/A (no TurboModule spec change)

@SomePersonFromMars
SomePersonFromMars force-pushed the fix/ios-recording-interrupted branch from 36d372e to 900e04b Compare August 28, 2026 12:06
@SomePersonFromMars
SomePersonFromMars force-pushed the fix/ios-recording-interrupted branch from 900e04b to 6283fb7 Compare August 28, 2026 12:08
@SomePersonFromMars
SomePersonFromMars force-pushed the fix/ios-recording-interrupted branch from 534c3c0 to 3bd07bf Compare September 2, 2026 12:06
@SomePersonFromMars
SomePersonFromMars force-pushed the fix/ios-recording-interrupted branch from 3bd07bf to 86a9ae7 Compare September 2, 2026 12:09
@SomePersonFromMars
SomePersonFromMars requested review from closetcaiman and maciejmakowski2003 and removed request for closetcaiman September 4, 2026 11:09
@SomePersonFromMars
SomePersonFromMars marked this pull request as ready for review September 4, 2026 11:09
@SomePersonFromMars SomePersonFromMars changed the title Fix/ios recording interrupted fix(ios): recording resume after an interruption Sep 4, 2026

@maciejmakowski2003 maciejmakowski2003 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could you perform the same research on android? in perfect world we would like to provide SotA for interruptions om both platforms.

Comment thread apps/CLAUDE.md

Enable emission with `AudioManager.observeAudioInterruptions(true)`, then listen.

**Playback:** pause on `began` (native does not resume players). The AudioFile example resumes on `ended` when it had been playing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
**Playback:** pause on `began` (native does not resume players). The AudioFile example resumes on `ended` when it had been playing.
**Playback:** native pause on `began`, but does not resume any audio contexts. The AudioFile example resumes on `ended` when it had been playing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could you research and add a section about background resume for playback? I am quite sure that below audio session settings allow it.

AudioManager.setAudioSessionOptions({
    iosCategory: 'playback',
    iosMode: 'spokenAudio',
  })

Comment on lines +131 to +140
- (void)emitInterruptionBeganIfAccepted:(bool)accepted
{
if (!self.audioInterruptionsObserved || !accepted) {
return;
}

[self.audioAPIModule invokeHandlerWithEventName:audioapi::AudioEvent::INTERRUPTION
payload:audioapi::InterruptionPayload{
.type = "began", .shouldResume = false}];
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

let's inline it

AudioEngine *audioEngine = self.audioAPIModule.audioEngine;

if (self.interruptionEndedDelivered && [audioEngine getState] == AudioEngineStateInterrupted) {
[self performInterruptionEndOnEngine:audioEngine shouldResume:true];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

verify hardcoded shouldResume: true

Comment on lines +442 to +452
if (!shouldResume && self.inputRegistration == nil) {
[self stopEngine];
[self rebuildAudioEngine];
self.state = AudioEngineState::AudioEngineStatePaused;
[self notifyInput:AudioEngineInputNotificationHardwareChanged];
return AudioEngineInterruptionEndOutcomePaused;
}

if (![self.sessionManager ensureActive:true error:&error]) {
NSLog(@"Error while activating audio session after interruption: %@", [error debugDescription]);
return AudioEngineInterruptionEndOutcomeStillInterrupted;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what if we have both player and recorder paused <==> engine paused and interruption happens? does it mean that engine will auto-start after interruption

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: recording never resumes after an interruption (onInterruptionEnd starts the engine without activating the audio session)

2 participants