Skip to content

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

Merged
mdydek merged 33 commits into
mainfrom
fix/ios-recording-interrupted
Oct 22, 2026
Merged

mdydek merged 33 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 pauses on began, but it does not resume audio contexts.

Introduced changes

Fixes iOS recording that never came back after an interruption. Updated library policy: the same take resumes when the app is in the foreground, or returns to it. 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. The take is left as it was, and a later retry can run.
  • shouldResume, according to the docs, only refers to playback resume. We choose to resume recording unconditionally, assuming that the user explicitly started it. The only exception is when playback should not be resumed: ideally, we would resume recording and leave playback paused. That is not trivial to implement, and the use case is uncommon, so it is safer not to resume in that case. A possible implementation is currently under development in Refactor/ios interruption policy #1346.

§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. The retry uses a cached shouldResume flag.
  • Hardware change notification before starting the engine. notifyInput:HardwareChanged is called before the engine starts, so the recorder matches the new format before the first buffer arrives.
  • 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 Outdated
Comment thread packages/audiodocs/docs/system/audio-manager.mdx
Comment thread packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm Outdated
@SomePersonFromMars SomePersonFromMars added research Information gathering, technical analysis, or feasibility studies fix Code changes specifically addressing and resolving a bug ios Native iOS implementation, CoreAudio/AVFoundation bindings, or iOS-specific issues labels Sep 7, 2026
@github-actions github-actions Bot added the conflicts This PR has conflicts with the base branch. label Sep 11, 2026

@mdydek mdydek left a comment

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.

I think we should in different PR implement also recoverable android from the interruption, because right now the only thing it does is sending event to the js about the interruption which would make both platforms behavior non uniform

Comment thread packages/audiodocs/docs/other/audio-api-plugin.mdx
Comment thread packages/audiodocs/docs/system/audio-manager.mdx Outdated
Comment thread packages/audiodocs/docs/system/audio-manager.mdx Outdated
Comment thread packages/audiodocs/docs/system/audio-manager.mdx Outdated
Comment thread packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm Outdated
Comment thread packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm Outdated
@github-actions github-actions Bot added the conflicts This PR has conflicts with the base branch. label Oct 2, 2026
@github-actions github-actions Bot added conflicts This PR has conflicts with the base branch. and removed conflicts This PR has conflicts with the base branch. labels Oct 12, 2026
@SomePersonFromMars
SomePersonFromMars removed this pull request from stack #1318 October 14, 2026 16:31
@SomePersonFromMars
SomePersonFromMars added this pull request to stack #1347 October 14, 2026 16:34
Comment thread packages/audiodocs/docs/system/audio-manager.mdx
Comment thread packages/audiodocs/docs/system/audio-manager.mdx
Comment thread packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h Outdated
@closetcaiman closetcaiman mentioned this pull request Oct 19, 2026
7 tasks
@github-actions github-actions Bot removed the conflicts This PR has conflicts with the base branch. label Oct 19, 2026
@SomePersonFromMars
SomePersonFromMars removed this pull request from stack #1347 October 19, 2026 13:47
@SomePersonFromMars
SomePersonFromMars added this pull request to stack #1353 October 19, 2026 13:47
Comment thread packages/audiodocs/docs/system/audio-manager.mdx Outdated
Comment thread packages/internaldocs/docs/system/interruptions.mdx
Comment thread packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioFileWriter.h Outdated
Comment thread packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm Outdated
Comment thread packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm Outdated
Comment thread packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm Outdated
@github-actions github-actions Bot added the conflicts This PR has conflicts with the base branch. label Oct 20, 2026
@github-actions github-actions Bot removed the conflicts This PR has conflicts with the base branch. label Oct 22, 2026
@mdydek
mdydek merged commit f42b008 into main Oct 22, 2026
16 checks passed
@mdydek
mdydek deleted the fix/ios-recording-interrupted branch October 22, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Code changes specifically addressing and resolving a bug ios Native iOS implementation, CoreAudio/AVFoundation bindings, or iOS-specific issues research Information gathering, technical analysis, or feasibility studies

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)

5 participants