Platform: iOS
Flutter model: 3.29.3
Packages used:
- file: ^6.0.0
- audio_session: ^0.2.1
✅ Anticipated Behaviour
- Microphone Availability Examine
- Earlier than beginning or resuming recording, examine if the microphone is already in use by one other app.
- If unavailable, show a dialog: “Microphone is at the moment in use by one other software.”
- Background Recording Assist
- Permit ongoing recordings to proceed even when the app is shipped to the background, guaranteeing persistent audio seize.
- Automated Interruption Dealing with
- Cellphone/VOIP Name Interruption: Robotically pause recording when a telephone or VOIP (e.g., WhatsApp, Groups) name is acquired or ongoing.
- System Focus Modes:
- Pause recording if the system is in a spotlight mode (like Do Not Disturb) and the microphone will not be obtainable.
- Resume recording by person manually ought to solely resume if mic is accessible.
- Resume mechanically if interruption ends.
- Auto Resume Recording
- Robotically resume recording as soon as the interruption (name or focus mode) ends, sustaining the session seamlessly.
❌ Precise Behaviour
- You acquired a Groups name whereas recording was on-going.
- Whereas a Groups name is ringing (not accepted), the recording continues and the mic orange dot stays energetic.
- Whenever you declined the decision, the recording paused mechanically (on account of an “interruption occasion”)
- Whenever you tried to renew recording, the microphone labored briefly however then stopped
- After pausing and making an attempt to renew once more, you acquired the error: PlatformException(file, The operation could not be accomplished. (com.apple.coreaudio.avfaudio error -10868.), null, null)
🔁 Steps to Reproduce the Concern
- Begin recording within the app (orange mic indicator seems)
- Obtain an incoming Groups name (recording continues)
- Decline the Groups name (recording mechanically pauses on account of interruption dealing with)
- Try and resume recording (microphone works briefly then fails)
- Pause recording manually
- Try and resume recording once more (error -10868 seems)
🔍 Associated Code Snippets
- Arrange the recorder and audio session throughout initialisation within the initState methodology.
/*
_initialiseRecorderController is used to initialize recorder controller and audio session
*/
void _initialiseRecorderController() async {
_audioRecorder = AudioRecorder();
_session = await AudioSession.occasion;
await _configureAudioSession();
}
- Configure the audio_session
Future _configureAudioSession() async {
strive
AVAudioSessionCategoryOptions.allowBluetoothA2dp catch (e) {
debugPrint("_configureAudioSession error : ${e.toString()}");
}
}
- The toggle recording operate is triggered from the recorder button throughout the construct methodology.
/*
_toggleRecording is operate that begins the recording if the
microphone request is granted and request permission if not.
*/
Future _toggleRecording() async {
strive {
bool isMicrophoneAvailable = await _checkMicrophoneAvailability();
if (!isMicrophoneAvailable) {
await _showMicrophoneUnavailableDialog();
return;
}
if (await Permission.microphone.isGranted) {
if (_isRecordingStarted) {
await _resumePauseRecording();
} else {
await WakelockPlus.allow();
await _startRecording();
}
} else {
ultimate bool isPermissionGranted = await ServiceUtils.requestPermission(
Permission.microphone,
);
if (isPermissionGranted) {
await _toggleRecording();
}
}
} catch (e) {
debugPrint("_toggleRecording error : ${e.toString()}");
}
}
- Examine microphone availability each at first and when resuming recording.
Future _checkMicrophoneAvailability() async {
strive {
if (_isMicrophoneInUse && Platform.isAndroid) {
debugPrint("Microphone is at the moment in use by one other app.");
return false;
}
ultimate bool isActive = await _requestAudioFocus();
if (!isActive) {
await _session.setActive(false);
debugPrint("Microphone is unavailable. One other app is utilizing it.");
return false;
}
return true;
} catch (e) {
await _session.setActive(false);
debugPrint("Error checking microphone availability: ${e.toString()}");
return false;
}
}
- Begin the recording
/*
_startRecording operate calls the _startBackupRecording with the beginning
of the foreground service and connecting to the socket
*/
Future _startRecording() async {
strive {
_meetingStartTime = DateTime.now().millisecondsSinceEpoch;
_startForegroundTask();
await _socketConnection();
_recordDuration = 0;
_startTimer();
await _startBackupRecording();
setState(() {
_isRecording = true;
_isRecordingStarted = true;
_isRecordingCompleted = false;
});
} catch (e) {
debugPrint("_startRecording error : ${e.toString()}");
}
}
/*
_startBackupRecording begins the recording utilizing startStream and
_recordStream is listened so as to add audio knowledge to the _recordedData constantly
and a _backupTimer is began for creating 30 secs chunk file
*/
Future _startBackupRecording() async {
strive {
await _configureAudioSession();
bool energetic = await _requestAudioFocus();
if (!energetic) return; // Forestall beginning if focus not acquired
await _handleAudioInterruptions();
_recordStream = await _audioRecorder.startStream(
const RecordConfig(
encoder: AudioEncoder.pcm16bits,
sampleRate: 22000,
numChannels: 1,
// bluetoothSco: true,
autoGain: true,
),
);
_recordStream?.hear((occasion) {
_recordedData.add(occasion);
});
_startBackupTimer();
} catch (e) {
debugPrint("Error startBackupRecording: ${e.toString()}");
}
}
- Request audio focus
/*
_requestAudioFocus is used to request audio focus when app begins recording
*/
Future _requestAudioFocus() async {
strive {
return await _session.setActive(
true,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation,
);
} catch (e) {
return false;
}
}
- Deal with Audio Interruption
/*
_handleAudioInterruptions is used to detect and deal with interruptions when one other app makes use of microphone
*/
Future _handleAudioInterruptions() async {
_recordInterruptionSubscription = _session.interruptionEventStream.hear((occasion) async {
if (occasion.start) {
setState(() {
_isMicrophoneInUse = true;
});
} else {
setState(() {
_isMicrophoneInUse = false;
});
}
});
}
- Resume And Pause the recording
/*
_resumePauseRecording is used to renew and pause the recording.
When paused the _recordDuration _timer and _backupTimer is stopped.
When resumed the _recordDuration _timer and _backupTimer is began once more.
*/
Future _resumePauseRecording() async {
if (_isRecording) {
// pause recording
} else {
// resume recording
}
}
/*
_resumeRecording is used to renew the recording.
When resumed the _recordDuration _timer and _backupTimer is began once more.
*/
Future _resumeRecording() async {
strive {
await _configureAudioSession(); // 🔁 reconfigure session earlier than resuming
bool energetic = await _requestAudioFocus();
if (!energetic) {
return;
}
await Future.delayed(Durations.medium1);
await _audioRecorder.resume();
_startTimer();
_startBackupTimer();
setState(() {
_isRecording = true;
_isPause = false;
_isManuallyPause = false;
});
} catch (e) {
}
}
/*
_pauseRecording is used to pause the recording and add that a part of chunk file.
When paused the _recordDuration _timer and _backupTimer is stopped.
*/
Future _pauseRecording({
bool isManuallyPause = false,
}) async {
strive {
_timer?.cancel();
_backupTimer?.cancel();
await _audioRecorder.pause();
setState(() {
_isRecording = false;
_isPause = true;
_isManuallyPause = isManuallyPause;
});
} catch (e) {
debugPrint("Error _pauseRecording: ${e.toString()}");
}
}
- Cease the recording
/*
_stopRecording stops the recording and stops the foreground service
*/
Future _stopRecording() async {
strive {
await _audioRecorder.cease();
_timer?.cancel();
_stopForegroundTask();
// Add a brief delay earlier than deactivating the session
await Future.delayed(const Period(milliseconds: 200));
await _deactivateAudioSession();
setState(() {
_isRecording = false;
_isRecordingStarted = false;
_isPause = false;
_isManuallyPause = false;
_isRecordingCompleted = true;
_isMicrophoneInUse = false;
});
} catch (e) {
debugPrint("Error stopping the recording: ${e.toString()}");
}
}
- Deactivate Audio Session
Future _deactivateAudioSession() async {
strive {
await _session.setActive(
false,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation,
);
} catch (e) {
debugPrint("Error deactivating audio session: ${e.toString()}");
}
}
🔄 Workaround Tried
- Manually deactivating and reactivating the session.
- Reinitializing the audio recorder occasion.
- Including an extended delay earlier than resuming.
- Re-requesting focus with setActive(true).
❓ Questions
- Is there any iOS-specific workaround for resuming the mic after a
declined VoIP name? - Is that this anticipated conduct in iOS or an edge case of audio session
state administration?