fix(ios): recording resume after an interruption - #1257
SomePersonFromMars wants to merge 22 commits into
Conversation
…ting a closed writer
36d372e to
900e04b
Compare
…s actually delivered
900e04b to
6283fb7
Compare
Also JS handler is only executed on an engine state change.
534c3c0 to
3bd07bf
Compare
3bd07bf to
86a9ae7
Compare
maciejmakowski2003
left a comment
There was a problem hiding this comment.
could you perform the same research on android? in perfect world we would like to provide SotA for interruptions om both platforms.
There was a problem hiding this comment.
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',
})There was a problem hiding this comment.
It is out of scope of this PR, so I will remember to do it in another one.
| - (void)emitInterruptionBeganIfAccepted:(bool)accepted | ||
| { | ||
| if (!self.audioInterruptionsObserved || !accepted) { | ||
| return; | ||
| } | ||
|
|
||
| [self.audioAPIModule invokeHandlerWithEventName:audioapi::AudioEvent::INTERRUPTION | ||
| payload:audioapi::InterruptionPayload{ | ||
| .type = "began", .shouldResume = false}]; | ||
| } |
There was a problem hiding this comment.
let's inline it
There was a problem hiding this comment.
Objective C doesn't really have inline methods, because sending messages always happens in runtime via dynamic dispatch. The closest thing we can get would be the following:
inline void privateHelper(MyClass *self, NSString *value) {
[self doSomething];
}
@implementation MyClass
- (void)publicMethod {
privateHelper(self, @"Hello");
}
@endI think, though, that it would introduce too much complexity.
| AudioEngine *audioEngine = self.audioAPIModule.audioEngine; | ||
|
|
||
| if (self.interruptionEndedDelivered && [audioEngine getState] == AudioEngineStateInterrupted) { | ||
| [self performInterruptionEndOnEngine:audioEngine shouldResume:true]; |
There was a problem hiding this comment.
verify hardcoded shouldResume: true
There was a problem hiding this comment.
Please verify the new caching policy.
| 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; |
There was a problem hiding this comment.
what if we have both player and recorder paused <==> engine paused and interruption happens? does it mean that engine will auto-start after interruption
There was a problem hiding this comment.
Please verify thew new more explicit playback/recording resume policy.
WPT non-regression comparisonFAIL — 6 file(s) with new failing subtests · 3 improved section(s) · overall 2717 → 3420 (+703)
New failing subtests:
Unchanged sections (25)
Baseline: Workflow run · this comment is updated on every push. |
…n device that it still works.
Co-authored-by: Maciej Makowski <maciej.makowski2608@gmail.com>
@maciejmakowski2003 It seems out of scope of this PR, so I will remember to do it in another one. |
Closes #1220
observeAudioInterruptions(true)no longer replaces native recovery. NativeonInterruptionBegin/onInterruptionEndalways run. The flag only enables JSinterruptionevents, which matches the existingAudioManagerdocs. Apps that subscribed because observing used to skip native resume should not own engine recovery for a recorder (in particular, do not callRecorder.pause()onbegan- that moves the engine toPausedand disables the native retry). Playback can still pause onbegan. Native pauses onbegan, 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, 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
onInterruptionEndnever activated the session; failure poisonedIdle; no input-node guardstartEngine).startAndReturnErrorfailure, stayInterruptedand return (do not setIdle). A later retry can run.Interruptedand treat it as capture lost (do not look like a successful start with no buffers).§2 Docs vs behavior for
observeAudioInterruptionsobserveAudioInterruptions, an appropriate handler runs only whenAudioEngineactually transitioned to a new state. For example, if the engine wasInterrupted, the handler runs on a successful transition toRunning.§3 No-op
setActive:falsestill ran deactivationsetAudioSessionActivity(false)only calls session-deactivation handling if the session was actually managed and active beforesetActive. It no longer transitionsInterruptedtoPaused, which used to disableonInterruptionEnd.§3 / footnote -
resume()/ storeRecordingon a dead engineresume()still does not have a return type, but it is now guaranteed thatisRecording()is true if and only if the audio engine is not in an interrupted state and the recorder is actually running.AudioEngine'sonInterruptionEndrecovery afterAVAudioSessionInterruptionTypeEndedcan 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.Consumer note -
mixWithOthers/CannotInterruptOthersplayAndRecordis required to avoidCannotInterruptOtherswhen activating in the background, and is not sufficient for background restart. The Record demo enablesmixWithOtherswithdefaultmode.Additional useful tweaks and fixes
WillEnterForegroundNotificationandDidBecomeActiveNotification. iOS guarantees that a foreground audio start succeeds. The retry uses a cachedshouldResumeflag.handleInputConfigurationChangeto handle errors. A failed engine resume now finalizes the audio recorder.HardwareChanged) must open a new rotating segment instead of callingrotateFiles()on a writer that is already closed.IOSRotatingFileWriter::reprepareStreamFormatreopens when the inner file is closed;rotateFiles()no-ops if nothing is open.startIfNecessary, matching the recorder. A later recorder start must not play leftover source audio.observeAudioInterruptions(true); freeze the waveform onbegan, unfreeze only onended.Docs
AudioManagersubsection "Resume recording after an interruption".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-exampleapp and the included Recorder demo.Checklist