Public Access
Implement meeting assistant v1
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using MeetingAssistant.Recording;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Hotkeys;
|
||||
|
||||
public sealed class GlobalHotkeyService : BackgroundService
|
||||
{
|
||||
private const int HotkeyId = 0x4D41;
|
||||
private const int WmHotkey = 0x0312;
|
||||
private const int WmQuit = 0x0012;
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly MeetingRecordingCoordinator coordinator;
|
||||
private readonly ILogger<GlobalHotkeyService> logger;
|
||||
private TaskCompletionSource? messageLoopCompletion;
|
||||
private uint messageThreadId;
|
||||
|
||||
public GlobalHotkeyService(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
MeetingRecordingCoordinator coordinator,
|
||||
ILogger<GlobalHotkeyService> logger)
|
||||
{
|
||||
this.options = options.Value;
|
||||
this.coordinator = coordinator;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
logger.LogInformation("Global hotkey registration is only supported on Windows");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
messageLoopCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var messageThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
RunMessageLoop(stoppingToken);
|
||||
messageLoopCompletion.TrySetResult();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
messageLoopCompletion.TrySetException(exception);
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Meeting Assistant Hotkey Loop"
|
||||
};
|
||||
|
||||
messageThread.Start();
|
||||
return messageLoopCompletion.Task;
|
||||
}
|
||||
|
||||
private void RunMessageLoop(CancellationToken stoppingToken)
|
||||
{
|
||||
var hotkey = HotkeyDefinition.Parse(options.Hotkey.Toggle);
|
||||
messageThreadId = GetCurrentThreadId();
|
||||
using var stoppingRegistration = stoppingToken.Register(() => PostQuitToMessageThread());
|
||||
|
||||
if (!RegisterHotKey(IntPtr.Zero, HotkeyId, (uint)hotkey.Modifiers, hotkey.VirtualKey))
|
||||
{
|
||||
logger.LogWarning("Could not register global hotkey {Hotkey}", options.Hotkey.Toggle);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Registered global hotkey {Hotkey}", options.Hotkey.Toggle);
|
||||
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested && GetMessage(out var message, IntPtr.Zero, 0, 0) > 0)
|
||||
{
|
||||
if (message.Message == WmHotkey)
|
||||
{
|
||||
_ = Task.Run(ToggleRecordingAsync, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnregisterHotKey(IntPtr.Zero, HotkeyId);
|
||||
messageThreadId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
PostQuitToMessageThread();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ToggleRecordingAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await coordinator.ToggleAsync(CancellationToken.None);
|
||||
logger.LogInformation("Hotkey toggled recording. IsRecording={IsRecording}", status.IsRecording);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Hotkey toggle failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void PostQuitToMessageThread()
|
||||
{
|
||||
var threadId = messageThreadId;
|
||||
if (threadId != 0)
|
||||
{
|
||||
PostThreadMessage(threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool PostThreadMessage(uint idThread, int msg, UIntPtr wParam, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeMessage
|
||||
{
|
||||
public IntPtr Hwnd;
|
||||
public int Message;
|
||||
public UIntPtr WParam;
|
||||
public IntPtr LParam;
|
||||
public uint Time;
|
||||
public NativePoint Point;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativePoint
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user