Public Access
146 lines
4.6 KiB
C#
146 lines
4.6 KiB
C#
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<MicrophoneAudioSource> logger;
|
|
private readonly TimeSpan recoveryDelay;
|
|
|
|
public MicrophoneAudioSource(
|
|
IMicrophoneCaptureSourceFactory captureSources,
|
|
ILogger<MicrophoneAudioSource> logger)
|
|
: this(captureSources, logger, DefaultRecoveryDelay)
|
|
{
|
|
}
|
|
|
|
internal MicrophoneAudioSource(
|
|
IMicrophoneCaptureSourceFactory captureSources,
|
|
ILogger<MicrophoneAudioSource> logger,
|
|
TimeSpan recoveryDelay)
|
|
{
|
|
this.captureSources = captureSources;
|
|
this.logger = logger;
|
|
this.recoveryDelay = recoveryDelay;
|
|
}
|
|
|
|
public IAsyncEnumerable<AudioChunk> CaptureAsync(CancellationToken cancellationToken)
|
|
{
|
|
return CaptureAsync(new MeetingAssistantOptions(), cancellationToken);
|
|
}
|
|
|
|
public async IAsyncEnumerable<AudioChunk> CaptureAsync(
|
|
MeetingAssistantOptions options,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
var failedAttempts = 0;
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
IAsyncEnumerator<AudioChunk>? 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<bool> WaitForRecoveryAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(recoveryDelay, cancellationToken);
|
|
return true;
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|