Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 25, 2025

On iOS, StopAsync() returns null on the first recording but works correctly on subsequent recordings. Android is unaffected.

Root Cause

The AudioRecorder constructor calls FinishSession() before any session is initialized, leaving the AVAudioSession in an inconsistent state for the first recording.

Changes

Session lifecycle

  • Remove premature FinishSession() call from constructor
  • Session now properly initialized on first StartAsync() without prior deactivation

Resource management

  • Extract cleanup logic into CleanupRecorderResources() to handle disposal consistently
  • Dispose previous recorder instances in StartAsync() before creating new ones
  • Reset all fields (recorder, destinationFilePath, finishedRecordingCompletionSource) after StopAsync() completes

Event handling

  • Unsubscribe from FinishedRecording event before disposal
  • Parameterize cleanup method to avoid redundant unsubscriptions
// Before: Constructor interfered with first recording
public AudioRecorder(AudioRecorderOptions options)
{
    this.audioRecorderOptions = options;
    ActiveSessionHelper.FinishSession(options);  // ❌ Premature
}

// After: Clean initialization
public AudioRecorder(AudioRecorderOptions options)
{
    this.audioRecorderOptions = options;
}

Impact

First recording on iOS now works correctly, matching Android behavior. No changes to other platforms.

Original prompt

This section details on the original issue you should resolve

<issue_title>iOS returns a null IAudioSource object after the first recording, subsequent recordings are OK, Android OK</issue_title>
<issue_description>StopAsync() works as expected with Android, but on IOS, the first recording StopAsync() returns a null object. Subsequent recordings return a usable IAudioSource object. I've tried the Plugin.Maui.Audio.Sample and I don't see any issues with it, so it points to my code, but I do not see any problems with it.
While recording, I am able to poll audioRecorder.IsRecording and see that it is true.
Under what circumstances does StopAsync() return null? See vStopRecord() below.

I have the following methods to start/stop recording:

public async Task vStartRecord() {
       if ( await CheckPermissionIsGrantedAsync<Microphone>() ) {
           audioRecorder = audioManager.CreateRecorder();
           if ( audioRecorder == null ) {
               var toast = Toast.Make("error creating audio recorder", CommunityToolkit.Maui.Core.ToastDuration.Long, 12);
               await toast.Show();
               return;
               }
           var toast2 = Toast.Make("audio recorder can record: " + audioRecorder.CanRecordAudio, CommunityToolkit.Maui.Core.ToastDuration.Long, 12);
           await toast2.Show();

           var options = new AudioRecorderOptions {
               Channels = ChannelType.Mono,
               BitDepth = BitDepth.Pcm16bit,
               Encoding = Plugin.Maui.Audio.Encoding.Wav,
               SampleRate = 8000,
               ThrowIfNotSupported = true,
               #if IOS || MACCATALYST
               Category = AVFoundation.AVAudioSessionCategory.PlayAndRecord
               #endif
               };

           try {
               await audioRecorder.StartAsync(options);
               }

           catch {
               var toast = Toast.Make("error starting audio recorder", CommunityToolkit.Maui.Core.ToastDuration.Long, 12);
               await toast.Show();
               return;
               }
           }
      
       else {
           var toast = Toast.Make("no permission to record", CommunityToolkit.Maui.Core.ToastDuration.Long, 12);
           await toast.Show();
           }
       }

   public async Task vStopRecord() {
       if ( audioRecorder != null ) {
           audioSource = await audioRecorder.StopAsync();
           if ( audioSource != null ) {
               var toast = Toast.Make("no audio recorded", CommunityToolkit.Maui.Core.ToastDuration.Long, 12);
               await toast.Show();
               }
           }
       }

```</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
</comments>


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits October 25, 2025 11:24
- Remove FinishSession call from constructor that was interfering with session state
- Add proper disposal of AVAudioRecorder instances before creating new ones
- Reset recorder fields to null after StopAsync completes successfully
- This ensures clean state for subsequent recordings

Co-authored-by: jfversluis <[email protected]>
Extract recorder cleanup into CleanupRecorderResources() method for better maintainability and consistency

Co-authored-by: jfversluis <[email protected]>
Add parameter to CleanupRecorderResources to control event unsubscription, avoiding duplicate unsubscribe calls

Co-authored-by: jfversluis <[email protected]>
Copilot AI changed the title [WIP] Fix null IAudioSource object on first iOS recording Fix iOS first recording returning null due to premature session termination Oct 25, 2025
Copilot AI requested a review from jfversluis October 25, 2025 11:34
Copilot finished work on behalf of jfversluis October 25, 2025 11:34
@jfversluis jfversluis requested a review from Copilot October 25, 2025 11:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a bug where the first audio recording on iOS returns null from StopAsync() while subsequent recordings work correctly. The root cause was a premature FinishSession() call in the constructor that left the AVAudioSession in an inconsistent state before the first recording started.

Key changes:

  • Removed premature FinishSession() call from the AudioRecorder constructor
  • Added CleanupRecorderResources() method to properly manage recorder lifecycle and prevent resource leaks
  • Implemented cleanup of previous recorder instances in StartAsync() and after StopAsync() completes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jfversluis jfversluis marked this pull request as ready for review October 25, 2025 20:56
@jfversluis
Copy link
Owner

Issue reporter resolved their issue so this one can be closed for now.

@jfversluis jfversluis closed this Nov 4, 2025
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 returns a null IAudioSource object after the first recording, subsequent recordings are OK, Android OK

2 participants