Flutter : ❗️Concern: Mic will not be activating after resuming the recording after iOS telephone/voip name interruption – AVFAudio error -10868

0
2
Flutter : ❗️Concern: Mic will not be activating after resuming the recording after iOS telephone/voip name interruption – AVFAudio error -10868


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

  1. 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();
  }
  1. Configure the audio_session
Future _configureAudioSession() async {
    strive 
              AVAudioSessionCategoryOptions.allowBluetoothA2dp  catch (e) {
      debugPrint("_configureAudioSession error : ${e.toString()}");
    }
  }
  1. 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()}");
    }
  }
  1. 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;
    }
  }
  1. 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()}");
    }
  }

  1. 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;
    }
  }
  1. 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;
        });
      }
    });
  }
  1. 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()}");
    }
  }
  1. 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()}");
    }
  }
  1. 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?

LEAVE A REPLY

Please enter your comment!
Please enter your name here