namespace MeetingAssistant.Recording; public sealed class MicrophoneSwitchMonitor { private readonly IMicrophoneDeviceProvider devices; private readonly IMicrophoneSwitchPromptService prompts; private readonly ILogger logger; private readonly TimeSpan pollInterval; private readonly TimeSpan promptLifetime; public MicrophoneSwitchMonitor( IMicrophoneDeviceProvider devices, IMicrophoneSwitchPromptService prompts, ILogger logger) : this(devices, prompts, logger, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(1)) { } internal MicrophoneSwitchMonitor( IMicrophoneDeviceProvider devices, IMicrophoneSwitchPromptService prompts, ILogger logger, TimeSpan pollInterval, TimeSpan promptLifetime) { this.devices = devices; this.prompts = prompts; this.logger = logger; this.pollInterval = pollInterval; this.promptLifetime = promptLifetime; } public async Task RunAsync(Action switchMicrophone, CancellationToken cancellationToken) { var known = (ReadDevices() ?? []).Select(device => device.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); var pending = new Dictionary(StringComparer.OrdinalIgnoreCase); try { while (true) { await Task.Delay(pollInterval, cancellationToken); var available = ReadDevices(); if (available is null) continue; var availableIds = available.Select(device => device.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var (id, prompt) in pending.ToArray()) { if (!availableIds.Contains(id)) await prompt.Cancellation.CancelAsync(); if (prompt.Task.IsCompleted) { prompt.Cancellation.Dispose(); pending.Remove(id); } } foreach (var device in available) { if (known.Add(device.Id)) { var promptCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); promptCancellation.CancelAfter(promptLifetime); pending.Add(device.Id, new PendingPrompt(promptCancellation, OfferAsync(device, switchMicrophone, promptCancellation.Token))); } } } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } finally { foreach (var prompt in pending.Values) await prompt.Cancellation.CancelAsync(); await Task.WhenAll(pending.Values.Select(prompt => prompt.Task)); foreach (var prompt in pending.Values) prompt.Cancellation.Dispose(); } } private async Task OfferAsync(MicrophoneDevice device, Action switchMicrophone, CancellationToken cancellationToken) { try { if (await prompts.ConfirmSwitchAsync(device, cancellationToken) && !cancellationToken.IsCancellationRequested && ReadDevices()?.Any(current => current.Id.Equals(device.Id, StringComparison.OrdinalIgnoreCase)) == true && !cancellationToken.IsCancellationRequested) { switchMicrophone(device, cancellationToken); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception exception) { logger.LogWarning(exception, "Could not offer microphone {MicrophoneName}", device.Name); } } private IReadOnlyList? ReadDevices() { try { return devices.GetAvailableMicrophones(); } catch (Exception exception) { logger.LogWarning(exception, "Could not check for newly available microphones"); return null; } } private sealed record PendingPrompt(CancellationTokenSource Cancellation, Task Task); }