using System.Runtime.CompilerServices; namespace MeetingAssistant.Recording; public sealed class MicrophoneAudioSource : IMeetingAudioSource { private static readonly TimeSpan DefaultRecoveryDelay = TimeSpan.FromSeconds(1); private readonly IMicrophoneCaptureSourceFactory captureSources; private readonly ILogger logger; private readonly TimeSpan recoveryDelay; public MicrophoneAudioSource( IMicrophoneCaptureSourceFactory captureSources, ILogger logger) : this(captureSources, logger, DefaultRecoveryDelay) { } internal MicrophoneAudioSource( IMicrophoneCaptureSourceFactory captureSources, ILogger logger, TimeSpan recoveryDelay) { this.captureSources = captureSources; this.logger = logger; this.recoveryDelay = recoveryDelay; } public IAsyncEnumerable CaptureAsync(CancellationToken cancellationToken) { return CaptureAsync(new MeetingAssistantOptions(), cancellationToken); } public async IAsyncEnumerable CaptureAsync( MeetingAssistantOptions options, [EnumeratorCancellation] CancellationToken cancellationToken) { var failedAttempts = 0; while (!cancellationToken.IsCancellationRequested) { IAsyncEnumerator? capture = null; Exception? failure = null; try { capture = captureSources .CreateCapture(options) .CaptureAsync(options, cancellationToken) .GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception exception) { failure = exception; } if (cancellationToken.IsCancellationRequested) { yield break; } if (capture is not null) { try { while (!cancellationToken.IsCancellationRequested) { var hasNext = false; try { hasNext = await capture.MoveNextAsync(); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception exception) { failure = exception; } if (cancellationToken.IsCancellationRequested || failure is not null || !hasNext) { break; } if (failedAttempts > 0) { logger.LogInformation( "Microphone capture recovered after {FailedAttemptCount} failed attempt(s)", failedAttempts); failedAttempts = 0; } yield return capture.Current; } } finally { try { await capture.DisposeAsync(); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception exception) { failure ??= exception; } } } if (cancellationToken.IsCancellationRequested) { yield break; } failedAttempts++; logger.LogWarning( failure, "Microphone capture stopped unexpectedly; re-resolving an available microphone in {RecoveryDelay}", recoveryDelay); if (!await WaitForRecoveryAsync(cancellationToken)) { yield break; } } } private async Task WaitForRecoveryAsync(CancellationToken cancellationToken) { try { await Task.Delay(recoveryDelay, cancellationToken); return true; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { return false; } } }