diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8dfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.vs +*.suo +*.user + +/bin +/obj diff --git a/Button.cs b/Button.cs new file mode 100644 index 0000000..a5677e4 --- /dev/null +++ b/Button.cs @@ -0,0 +1,67 @@ +namespace Zergatul.Obs.InputOverlay +{ + public enum Button + { + None, + + Esc, + F1, + F2, + F3, + F4, + + Tilde, + Num1, + Num2, + Num3, + Num4, + Num5, + Num6, + Num7, + Num8, + Num9, + Num0, + + Tab, + KeyQ, + KeyW, + KeyE, + KeyR, + KeyT, + KeyY, + KeyU, + KeyI, + KeyO, + KeyP, + + Caps, + KeyA, + KeyS, + KeyD, + KeyF, + KeyG, + KeyH, + KeyJ, + KeyK, + KeyL, + + Shift, + KeyZ, + KeyX, + KeyC, + KeyV, + KeyB, + KeyN, + KeyM, + + Ctrl, + Alt, + Space, + + Mouse1, + Mouse2, + Mouse3, + Mouse4, + Mouse5 + } +} \ No newline at end of file diff --git a/EmptyInputHook.cs b/EmptyInputHook.cs new file mode 100644 index 0000000..af3530a --- /dev/null +++ b/EmptyInputHook.cs @@ -0,0 +1,14 @@ +using System; + +namespace Zergatul.Obs.InputOverlay +{ + public class EmptyInputHook : IInputHook + { + public event EventHandler ButtonAction; + + public void Dispose() + { + + } + } +} \ No newline at end of file diff --git a/IInputHook.cs b/IInputHook.cs new file mode 100644 index 0000000..9ac1648 --- /dev/null +++ b/IInputHook.cs @@ -0,0 +1,9 @@ +using System; + +namespace Zergatul.Obs.InputOverlay +{ + public interface IInputHook : IDisposable + { + event EventHandler ButtonAction; + } +} \ No newline at end of file diff --git a/IWebSocketHandler.cs b/IWebSocketHandler.cs new file mode 100644 index 0000000..34f1ee6 --- /dev/null +++ b/IWebSocketHandler.cs @@ -0,0 +1,11 @@ +using System; +using System.Net.WebSockets; +using System.Threading.Tasks; + +namespace Zergatul.Obs.InputOverlay +{ + public interface IWebSocketHandler : IDisposable + { + Task HandleWebSocket(WebSocket ws); + } +} \ No newline at end of file diff --git a/InputHook.cs b/InputHook.cs new file mode 100644 index 0000000..f6c76b6 --- /dev/null +++ b/InputHook.cs @@ -0,0 +1,289 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Threading; + +namespace Zergatul.Obs.InputOverlay +{ + using static WinApi; + + public class InputHook : IInputHook + { + public event EventHandler ButtonAction; + + private ILogger _logger; + private readonly object _syncObject = new object(); + private readonly Queue _buttonEventsQueue = new Queue(); + private readonly BitArray _state = new BitArray(256); + private readonly AutoResetEvent _resetEvent = new AutoResetEvent(false); + private readonly Thread _thread; + private volatile bool _shutdown = false; + + private WindowsHook _keyboardHook; + private WindowsHook _mouseHook; + + public InputHook(ILogger logger) + { + _logger = logger; + + using (var identity = WindowsIdentity.GetCurrent()) + { + var principal = new WindowsPrincipal(identity); + if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) + _logger?.LogWarning("App is running under non-admin. Hooks will not work from applications running as admin."); + } + + _keyboardHook = new WindowsHook(WH_KEYBOARD_LL, KeyboardHookCallback, _logger); + _mouseHook = new WindowsHook(WH_MOUSE_LL, MouseHookCallback, _logger); + + _thread = new Thread(DequeueThread); + _thread.Start(); + } + + public void Dispose() + { + _shutdown = true; + Thread.MemoryBarrier(); + _resetEvent.Set(); + _thread.Join(); + + _keyboardHook?.Dispose(); + _mouseHook?.Dispose(); + + _keyboardHook = null; + _mouseHook = null; + } + + private void DequeueThread() + { + while (!_shutdown) + { + _resetEvent.WaitOne(); + if (_shutdown) + break; + + while (true) + { + ButtonEvent buttonEvent; + lock (_syncObject) + { + if (_buttonEventsQueue.Count == 0) + break; + + buttonEvent = _buttonEventsQueue.Dequeue(); + } + + ButtonAction?.Invoke(this, buttonEvent); + } + } + } + + private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode >= 0) + { + switch ((int)wParam) + { + case WM_KEYDOWN: + case WM_KEYUP: + int vkCode = Marshal.ReadInt32(lParam); + Button button = GetButtonFromVKCode(vkCode); + if (button != Button.None) + { + if (wParam == (IntPtr)WM_KEYDOWN) + ProcessKeyDown(button); + if (wParam == (IntPtr)WM_KEYUP) + ProcessKeyUp(button); + } + break; + + case WM_SYSKEYDOWN: + ProcessKeyDown(Button.Alt); + break; + + case WM_SYSKEYUP: + ProcessKeyUp(Button.Alt); + break; + } + } + return CallNextHookEx(_keyboardHook.HookHandle, nCode, wParam, lParam); + } + + private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode >= 0) + { + Button button = Button.None; + bool pressed = false; + switch ((int)wParam) + { + case WM_LBUTTONDOWN: + button = Button.Mouse1; + pressed = true; + break; + + case WM_LBUTTONUP: + button = Button.Mouse1; + pressed = false; + break; + + case WM_RBUTTONDOWN: + button = Button.Mouse2; + pressed = true; + break; + + case WM_RBUTTONUP: + button = Button.Mouse2; + pressed = false; + break; + + case WM_MBUTTONDOWN: + button = Button.Mouse3; + pressed = true; + break; + + case WM_MBUTTONUP: + button = Button.Mouse3; + pressed = false; + break; + + case WM_XBUTTONDOWN: + int mouseData = Marshal.ReadInt32(lParam + 8); + switch (mouseData >> 16) + { + case XBUTTON1: button = Button.Mouse4; break; + case XBUTTON2: button = Button.Mouse5; break; + } + pressed = true; + break; + + case WM_XBUTTONUP: + mouseData = Marshal.ReadInt32(lParam + 8); + switch (mouseData >> 16) + { + case XBUTTON1: button = Button.Mouse4; break; + case XBUTTON2: button = Button.Mouse5; break; + } + pressed = false; + break; + } + + if (button != Button.None) + { + lock (_syncObject) + { + _buttonEventsQueue.Enqueue(new ButtonEvent(button, pressed)); + _resetEvent.Set(); + } + } + } + + return CallNextHookEx(_mouseHook.HookHandle, nCode, wParam, lParam); + } + + private void ProcessKeyDown(Button button) + { + lock (_syncObject) + { + if (!_state[(int)button]) + { + _buttonEventsQueue.Enqueue(new ButtonEvent(button, true)); + _state[(int)button] = true; + _resetEvent.Set(); + } + } + } + + private void ProcessKeyUp(Button button) + { + lock (_syncObject) + { + _buttonEventsQueue.Enqueue(new ButtonEvent(button, false)); + _state[(int)button] = false; + _resetEvent.Set(); + } + } + + private Button GetButtonFromVKCode(int vkCode) + { + switch (vkCode) + { + case VK_LBUTTON: return Button.Mouse1; + case VK_RBUTTON: return Button.Mouse2; + case VK_MBUTTON: return Button.Mouse3; + case VK_XBUTTON1: return Button.Mouse4; + case VK_XBUTTON2: return Button.Mouse5; + case VK_TAB: return Button.Tab; + case VK_SHIFT: return Button.Shift; + case VK_CONTROL: return Button.Ctrl; + case VK_MENU: return Button.Alt; + case VK_CAPITAL: return Button.Caps; + case VK_ESCAPE: return Button.Esc; + case VK_SPACE: return Button.Space; + case VK_0: return Button.Num0; + case VK_1: return Button.Num1; + case VK_2: return Button.Num2; + case VK_3: return Button.Num3; + case VK_4: return Button.Num4; + case VK_5: return Button.Num5; + case VK_6: return Button.Num6; + case VK_7: return Button.Num7; + case VK_8: return Button.Num8; + case VK_9: return Button.Num9; + case VK_A: return Button.KeyA; + case VK_B: return Button.KeyB; + case VK_C: return Button.KeyC; + case VK_D: return Button.KeyD; + case VK_E: return Button.KeyE; + case VK_F: return Button.KeyF; + case VK_G: return Button.KeyG; + case VK_H: return Button.KeyH; + case VK_I: return Button.KeyI; + case VK_J: return Button.KeyJ; + case VK_K: return Button.KeyK; + case VK_L: return Button.KeyL; + case VK_M: return Button.KeyM; + case VK_N: return Button.KeyN; + case VK_O: return Button.KeyO; + case VK_P: return Button.KeyP; + case VK_Q: return Button.KeyQ; + case VK_R: return Button.KeyR; + case VK_S: return Button.KeyS; + case VK_T: return Button.KeyT; + case VK_U: return Button.KeyU; + case VK_V: return Button.KeyV; + case VK_W: return Button.KeyW; + case VK_X: return Button.KeyX; + case VK_Y: return Button.KeyY; + case VK_Z: return Button.KeyZ; + case VK_F1: return Button.F1; + case VK_F2: return Button.F2; + case VK_F3: return Button.F3; + case VK_F4: return Button.F4; + case VK_LSHIFT: return Button.Shift; + case VK_RSHIFT: return Button.Shift; + case VK_LCONTROL: return Button.Ctrl; + case VK_RCONTROL: return Button.Ctrl; + case VK_LMENU: return Button.Alt; + case VK_RMENU: return Button.Alt; + case VK_OEM_3: return Button.Tilde; + default: return Button.None; + } + } + } + + public struct ButtonEvent + { + public Button Button { get; } + public bool Pressed { get; } + + public ButtonEvent(Button button, bool pressed) + { + Button = button; + Pressed = pressed; + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..db84594 --- /dev/null +++ b/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using System; + +namespace Zergatul.Obs.InputOverlay +{ + class Program + { + [STAThread] + static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateWebHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(builder => + { + builder.UseStartup(); + builder.UseWebRoot("wwwroot"); + builder.UseUrls("http://localhost:5001/"); + builder.UseKestrel(); + }); + } + } +} \ No newline at end of file diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..7fcc621 --- /dev/null +++ b/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + False + False + True + Release + Any CPU + FileSystem + bin\Release\netcoreapp3.1\win-x64\publish\ + FileSystem + + \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..100e3e5 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "ZergatulInputOverlay": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..745f235 --- /dev/null +++ b/Startup.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Zergatul.Obs.InputOverlay +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + //services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app, IHostApplicationLifetime hostAppLifetime, IWebSocketHandler handler) + { + app.UseDefaultFiles(); + app.UseStaticFiles(); + app.UseWebSockets(); + + hostAppLifetime.ApplicationStopping.Register(() => + { + handler.Dispose(); + }); + + app.Use(async (context, next) => + { + if (context.Request.Path == "/ws" && context.WebSockets.IsWebSocketRequest) + { + using (var ws = await context.WebSockets.AcceptWebSocketAsync()) + { + await handler.HandleWebSocket(ws); + } + } + else + { + await next(); + } + }); + } + } +} \ No newline at end of file diff --git a/WebSocketHandler.cs b/WebSocketHandler.cs new file mode 100644 index 0000000..2f31e0a --- /dev/null +++ b/WebSocketHandler.cs @@ -0,0 +1,273 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Zergatul.Obs.InputOverlay +{ + public class WebSocketHandler : IWebSocketHandler + { + private readonly IInputHook _hook; + private readonly ILogger _logger; + private readonly object _syncObject = new object(); + private readonly Random _rnd = new Random(); + private List _webSockets = new List(); + private WebSocketWrapper[] _wsBuffer = new WebSocketWrapper[16]; + + public WebSocketHandler(IInputHook hook, ILogger logger) + { + _hook = hook; + _logger = logger; + + _hook.ButtonAction += Hook_ButtonAction; + } + + private async void Hook_ButtonAction(object sender, ButtonEvent e) + { + int count; + lock (_syncObject) + { + count = _webSockets.Count; + if (count > _wsBuffer.Length) + { + _logger.LogWarning("WebSockets count greater than buffer size."); + count = _wsBuffer.Length; + } + _webSockets.CopyTo(0, _wsBuffer, 0, count); + } + + int eventType = e.Button < Button.Mouse1 ? 1 : 2; + var json = JsonSerializer.Serialize(new JavascriptEvent + { + type = eventType, + button = e.Button.ToString(), + pressed = e.Pressed + }); + byte[] raw = Encoding.ASCII.GetBytes(json); + + for (int i = 0; i < count; i++) + { + var wrapper = _wsBuffer[i]; + if ((wrapper.EventMask & eventType) != 0) + { + try + { + await wrapper.WebSocket.SendAsync(new ReadOnlyMemory(raw), WebSocketMessageType.Text, true, CancellationToken.None); + } + catch (WebSocketException) + { + _logger?.LogInformation("WebSocketException on SendAsync."); + RemoveWebSocket(wrapper); + } + } + } + } + + public async Task HandleWebSocket(WebSocket ws) + { + var wrapper = new WebSocketWrapper + { + WebSocket = ws, + CancellationSource = new CancellationTokenSource() + }; + lock (_syncObject) + { + _webSockets.Add(wrapper); + } + + _logger?.LogInformation($"New WebSocket. Total WebSockets: {_webSockets.Count}."); + + var receive = ReceiveLoop(wrapper); + var ping = PingLoop(wrapper); + await Task.WhenAll(receive, ping); + + wrapper.CancellationSource.Dispose(); + _logger?.LogInformation("HandleWebSocket ended successfully."); + } + + public void Dispose() + { + _hook.Dispose(); + + int count; + lock (_syncObject) + { + count = _webSockets.Count; + _webSockets.CopyTo(_wsBuffer); + } + + for (int i = 0; i < count; i++) + { + RemoveWebSocket(_wsBuffer[i]); + } + + lock (_syncObject) + { + _webSockets.Clear(); + } + } + + private void RemoveWebSocket(WebSocketWrapper wrapper) + { + lock (wrapper) + { + if (wrapper.Closing) + return; + + wrapper.Closing = true; + } + + _logger?.LogInformation("WebSocket disconnected."); + + wrapper.CancellationSource.Cancel(); + + lock (_syncObject) + { + int index = _webSockets.IndexOf(wrapper); + if (index >= 0) + { + _webSockets.RemoveAt(index); + } + } + + _logger?.LogInformation($"Total WebSockets: {_webSockets.Count}."); + } + + private async Task ReceiveLoop(WebSocketWrapper wrapper) + { + try + { + while (true) + { + var segment = new ArraySegment(new byte[256]); + + WebSocketReceiveResult result; + try + { + result = await wrapper.WebSocket.ReceiveAsync(segment, wrapper.CancellationSource.Token); + } + catch (OperationCanceledException) + { + return; + } + catch (WebSocketException) + { + _logger?.LogInformation("WebSocketException on ReceiveAsync."); + RemoveWebSocket(wrapper); + return; + } + + if (result.Count == 0) + { + _logger?.LogInformation("Empty response."); + RemoveWebSocket(wrapper); + return; + } + + var evt = JsonSerializer.Deserialize(segment.AsSpan(0, result.Count)); + if (evt.eventMask != null) + { + wrapper.EventMask = evt.eventMask.Value; + } + if (evt.ping != null) + { + if (wrapper.LastPing != evt.ping) + { + _logger?.LogInformation("Receive ping data doesn't match."); + RemoveWebSocket(wrapper); + return; + } + wrapper.LastPing = null; + } + } + } + catch (Exception ex) + { + _logger?.LogError("ReceiveLoop -> " + ex.GetType().ToString() + " " + ex.Message); + } + } + + private async Task PingLoop(WebSocketWrapper wrapper) + { + try + { + while (true) + { + if (wrapper.LastPing != null) + { + _logger?.LogInformation("Ping response not received;"); + RemoveWebSocket(wrapper); + return; + } + + lock (_rnd) + { + wrapper.LastPing = _rnd.Next(); + } + + var json = JsonSerializer.Serialize(new JavascriptEvent + { + type = 0, + ping = wrapper.LastPing + }); + byte[] raw = Encoding.ASCII.GetBytes(json); + + try + { + await wrapper.WebSocket.SendAsync(new ReadOnlyMemory(raw), WebSocketMessageType.Text, true, wrapper.CancellationSource.Token); + } + catch (OperationCanceledException) + { + return; + } + catch (WebSocketException) + { + _logger?.LogInformation("WebSocketException on SendAsync."); + RemoveWebSocket(wrapper); + return; + } + + try + { + await Task.Delay(1000, wrapper.CancellationSource.Token); + } + catch (OperationCanceledException) + { + return; + } + } + } + catch (Exception ex) + { + _logger?.LogError("PingLoop -> " + ex.GetType().ToString() + " " + ex.Message); + } + } + + private class WebSocketWrapper + { + public WebSocket WebSocket; + public int EventMask; + public CancellationTokenSource CancellationSource; + public int? LastPing; + public volatile bool Closing; + } + + private class JavascriptEvent + { + public int type { get; set; } + public string button { get; set; } + public bool pressed { get; set; } + public int? ping { get; set; } + } + + private class ClientEvent + { + public int? eventMask { get; set; } + public int? ping { get; set; } + } + } +} \ No newline at end of file diff --git a/WinApi.cs b/WinApi.cs new file mode 100644 index 0000000..1889e1c --- /dev/null +++ b/WinApi.cs @@ -0,0 +1,223 @@ +using System; +using System.Runtime.InteropServices; + +namespace Zergatul.Obs.InputOverlay +{ + public static class WinApi + { + public const int WH_MOUSE = 7; + public const int WH_KEYBOARD_LL = 13; + public const int WH_MOUSE_LL = 14; + public const int WM_KEYDOWN = 0x0100; + public const int WM_KEYUP = 0x0101; + public const int WM_SYSKEYDOWN = 0x0104; + public const int WM_SYSKEYUP = 0x0105; + public const int WM_MOUSEMOVE = 0x0200; + public const int WM_LBUTTONDOWN = 0x0201; + public const int WM_LBUTTONUP = 0x0202; + public const int WM_RBUTTONDOWN = 0x0204; + public const int WM_RBUTTONUP = 0x0205; + public const int WM_MBUTTONDOWN = 0x0207; + public const int WM_MBUTTONUP = 0x0208; + public const int WM_XBUTTONDOWN = 0x020B; + public const int WM_XBUTTONUP = 0x020C; + + public const int XBUTTON1 = 0x0001; + public const int XBUTTON2 = 0x0002; + + #region VK + + public const int VK_LBUTTON = 0x01; + public const int VK_RBUTTON = 0x02; + public const int VK_CANCEL = 0x03; + public const int VK_MBUTTON = 0x04; + public const int VK_XBUTTON1 = 0x05; + public const int VK_XBUTTON2 = 0x06; + public const int VK_BACK = 0x08; + public const int VK_TAB = 0x09; + public const int VK_CLEAR = 0x0C; + public const int VK_RETURN = 0x0D; + public const int VK_SHIFT = 0x10; + public const int VK_CONTROL = 0x11; + public const int VK_MENU = 0x12; + public const int VK_PAUSE = 0x13; + public const int VK_CAPITAL = 0x14; + public const int VK_KANA = 0x15; + public const int VK_HANGUEL = 0x15; + public const int VK_HANGUL = 0x15; + public const int VK_IME_ON = 0x16; + public const int VK_JUNJA = 0x17; + public const int VK_FINAL = 0x18; + public const int VK_HANJA = 0x19; + public const int VK_KANJI = 0x19; + public const int VK_IME_OFF = 0x1A; + public const int VK_ESCAPE = 0x1B; + public const int VK_CONVERT = 0x1C; + public const int VK_NONCONVERT = 0x1D; + public const int VK_ACCEPT = 0x1E; + public const int VK_MODECHANGE = 0x1F; + public const int VK_SPACE = 0x20; + public const int VK_PRIOR = 0x21; + public const int VK_NEXT = 0x22; + public const int VK_END = 0x23; + public const int VK_HOME = 0x24; + public const int VK_LEFT = 0x25; + public const int VK_UP = 0x26; + public const int VK_RIGHT = 0x27; + public const int VK_DOWN = 0x28; + public const int VK_SELECT = 0x29; + public const int VK_PRINT = 0x2A; + public const int VK_EXECUTE = 0x2B; + public const int VK_SNAPSHOT = 0x2C; + public const int VK_INSERT = 0x2D; + public const int VK_DELETE = 0x2E; + public const int VK_HELP = 0x2F; + public const int VK_0 = 0x30; + public const int VK_1 = 0x31; + public const int VK_2 = 0x32; + public const int VK_3 = 0x33; + public const int VK_4 = 0x34; + public const int VK_5 = 0x35; + public const int VK_6 = 0x36; + public const int VK_7 = 0x37; + public const int VK_8 = 0x38; + public const int VK_9 = 0x39; + public const int VK_A = 0x41; + public const int VK_B = 0x42; + public const int VK_C = 0x43; + public const int VK_D = 0x44; + public const int VK_E = 0x45; + public const int VK_F = 0x46; + public const int VK_G = 0x47; + public const int VK_H = 0x48; + public const int VK_I = 0x49; + public const int VK_J = 0x4A; + public const int VK_K = 0x4B; + public const int VK_L = 0x4C; + public const int VK_M = 0x4D; + public const int VK_N = 0x4E; + public const int VK_O = 0x4F; + public const int VK_P = 0x50; + public const int VK_Q = 0x51; + public const int VK_R = 0x52; + public const int VK_S = 0x53; + public const int VK_T = 0x54; + public const int VK_U = 0x55; + public const int VK_V = 0x56; + public const int VK_W = 0x57; + public const int VK_X = 0x58; + public const int VK_Y = 0x59; + public const int VK_Z = 0x5A; + public const int VK_LWIN = 0x5B; + public const int VK_RWIN = 0x5C; + public const int VK_APPS = 0x5D; + public const int VK_SLEEP = 0x5F; + public const int VK_NUMPAD0 = 0x60; + public const int VK_NUMPAD1 = 0x61; + public const int VK_NUMPAD2 = 0x62; + public const int VK_NUMPAD3 = 0x63; + public const int VK_NUMPAD4 = 0x64; + public const int VK_NUMPAD5 = 0x65; + public const int VK_NUMPAD6 = 0x66; + public const int VK_NUMPAD7 = 0x67; + public const int VK_NUMPAD8 = 0x68; + public const int VK_NUMPAD9 = 0x69; + public const int VK_MULTIPLY = 0x6A; + public const int VK_ADD = 0x6B; + public const int VK_SEPARATOR = 0x6C; + public const int VK_SUBTRACT = 0x6D; + public const int VK_DECIMAL = 0x6E; + public const int VK_DIVIDE = 0x6F; + public const int VK_F1 = 0x70; + public const int VK_F2 = 0x71; + public const int VK_F3 = 0x72; + public const int VK_F4 = 0x73; + public const int VK_F5 = 0x74; + public const int VK_F6 = 0x75; + public const int VK_F7 = 0x76; + public const int VK_F8 = 0x77; + public const int VK_F9 = 0x78; + public const int VK_F10 = 0x79; + public const int VK_F11 = 0x7A; + public const int VK_F12 = 0x7B; + public const int VK_F13 = 0x7C; + public const int VK_F14 = 0x7D; + public const int VK_F15 = 0x7E; + public const int VK_F16 = 0x7F; + public const int VK_F17 = 0x80; + public const int VK_F18 = 0x81; + public const int VK_F19 = 0x82; + public const int VK_F20 = 0x83; + public const int VK_F21 = 0x84; + public const int VK_F22 = 0x85; + public const int VK_F23 = 0x86; + public const int VK_F24 = 0x87; + public const int VK_NUMLOCK = 0x90; + public const int VK_SCROLL = 0x91; + public const int VK_LSHIFT = 0xA0; + public const int VK_RSHIFT = 0xA1; + public const int VK_LCONTROL = 0xA2; + public const int VK_RCONTROL = 0xA3; + public const int VK_LMENU = 0xA4; + public const int VK_RMENU = 0xA5; + public const int VK_BROWSER_BACK = 0xA6; + public const int VK_BROWSER_FORWARD = 0xA7; + public const int VK_BROWSER_REFRESH = 0xA8; + public const int VK_BROWSER_STOP = 0xA9; + public const int VK_BROWSER_SEARCH = 0xAA; + public const int VK_BROWSER_FAVORITES = 0xAB; + public const int VK_BROWSER_HOME = 0xAC; + public const int VK_VOLUME_MUTE = 0xAD; + public const int VK_VOLUME_DOWN = 0xAE; + public const int VK_VOLUME_UP = 0xAF; + public const int VK_MEDIA_NEXT_TRACK = 0xB0; + public const int VK_MEDIA_PREV_TRACK = 0xB1; + public const int VK_MEDIA_STOP = 0xB2; + public const int VK_MEDIA_PLAY_PAUSE = 0xB3; + public const int VK_LAUNCH_MAIL = 0xB4; + public const int VK_LAUNCH_MEDIA_SELECT = 0xB5; + public const int VK_LAUNCH_APP1 = 0xB6; + public const int VK_LAUNCH_APP2 = 0xB7; + public const int VK_OEM_1 = 0xBA; + public const int VK_OEM_PLUS = 0xBB; + public const int VK_OEM_COMMA = 0xBC; + public const int VK_OEM_MINUS = 0xBD; + public const int VK_OEM_PERIOD = 0xBE; + public const int VK_OEM_2 = 0xBF; + public const int VK_OEM_3 = 0xC0; + public const int VK_OEM_4 = 0xDB; + public const int VK_OEM_5 = 0xDC; + public const int VK_OEM_6 = 0xDD; + public const int VK_OEM_7 = 0xDE; + public const int VK_OEM_8 = 0xDF; + public const int VK_OEM_102 = 0xE2; + public const int VK_PROCESSKEY = 0xE5; + public const int VK_PACKET = 0xE7; + public const int VK_ATTN = 0xF6; + public const int VK_CRSEL = 0xF7; + public const int VK_EXSEL = 0xF8; + public const int VK_EREOF = 0xF9; + public const int VK_PLAY = 0xFA; + public const int VK_ZOOM = 0xFB; + public const int VK_NONAME = 0xFC; + public const int VK_PA1 = 0xFD; + public const int VK_OEM_CLEAR = 0xFE; + + #endregion + + public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + } +} \ No newline at end of file diff --git a/WindowsHook.cs b/WindowsHook.cs new file mode 100644 index 0000000..3ba52ff --- /dev/null +++ b/WindowsHook.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using static Zergatul.Obs.InputOverlay.WinApi; + +namespace Zergatul.Obs.InputOverlay +{ + sealed class WindowsHook : IDisposable + { + public IntPtr HookHandle { get; private set; } + + private readonly object _syncObject = new object(); + private readonly ILogger _logger; + private HookProc _proc; + + public WindowsHook(int type, HookProc proc, ILogger logger = null) + { + if (proc == null) + throw new ArgumentNullException(nameof(proc)); + + _proc = proc; + _logger = logger; + + using (var process = Process.GetCurrentProcess()) + using (var module = process.MainModule) + { + HookHandle = SetWindowsHookEx(type, _proc, GetModuleHandle(module.ModuleName), 0); + } + + if (HookHandle == IntPtr.Zero) + throw new InvalidOperationException("Cannot set hook."); + else + _logger?.LogInformation("Hook set: " + FormatIntPtr(HookHandle)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (HookHandle != IntPtr.Zero) + { + lock (_syncObject) + { + if (HookHandle != IntPtr.Zero) + { + if (UnhookWindowsHookEx(HookHandle)) + _logger?.LogInformation("Hook released."); + else + _logger.LogWarning("Cannot release hook."); + + HookHandle = IntPtr.Zero; + } + } + } + } + + ~WindowsHook() + { + Dispose(false); + } + + private static string FormatIntPtr(IntPtr ptr) + { + return "0x" + ptr.ToInt64().ToString("x2").PadLeft(16, '0'); + } + } +} \ No newline at end of file diff --git a/Zergatul.Obs.InputOverlay.csproj b/Zergatul.Obs.InputOverlay.csproj new file mode 100644 index 0000000..599e11b --- /dev/null +++ b/Zergatul.Obs.InputOverlay.csproj @@ -0,0 +1,37 @@ + + + + Exe + netcoreapp3.1 + Zergatul.Obs.InputOverlay + Zergatul.Obs.InputOverlay + Zergatul + true + true + win-x64 + true + true + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file diff --git a/Zergatul.Obs.InputOverlay.sln b/Zergatul.Obs.InputOverlay.sln new file mode 100644 index 0000000..068c180 --- /dev/null +++ b/Zergatul.Obs.InputOverlay.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31005.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Zergatul.Obs.InputOverlay", "Zergatul.Obs.InputOverlay.csproj", "{75165D08-FD2E-4ABA-B5AD-A5C510242E53}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {75165D08-FD2E-4ABA-B5AD-A5C510242E53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75165D08-FD2E-4ABA-B5AD-A5C510242E53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75165D08-FD2E-4ABA-B5AD-A5C510242E53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75165D08-FD2E-4ABA-B5AD-A5C510242E53}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {653FED5B-CDAC-45FC-88C9-BF6C456AC745} + EndGlobalSection +EndGlobal diff --git a/wwwroot/engine.js b/wwwroot/engine.js new file mode 100644 index 0000000..c9d43d2 --- /dev/null +++ b/wwwroot/engine.js @@ -0,0 +1,78 @@ +(function () { + var addSvgReleased = function (element) { + var animate = document.createElementNS('http://www.w3.org/2000/svg', 'animate'); + animate.setAttribute('id', element.id + '-animate'); + animate.setAttribute('attributeType', 'XML'); + animate.setAttribute('attributeName', 'fill-opacity'); + animate.setAttribute('values', '1.0; 0.0'); + animate.setAttribute('dur', '250ms'); + animate.setAttribute('fill', 'freeze'); + + element.appendChild(animate); + animate.beginElement(); + }; + + var removeSvgReleased = function (element) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + }; + + var state = {}; + + window.listenEvents = function (eventType) { + var ws = new WebSocket('ws://localhost:5001/ws'); + ws.onopen = function () { + ws.send(JSON.stringify({ eventMask: eventType })); + }; + ws.onmessage = function (event) { + var data = JSON.parse(event.data); + + if (data.type == 0) { + ws.send(JSON.stringify({ ping: data.ping })); + } + + if (eventType == 1 && data.type == 1) { + if (data.pressed) { + state[data.button] = true; + var element = document.getElementById(data.button); + if (element) { + element.classList.remove('released'); + element.classList.add('pressed'); + } + } else { + if (state[data.button]) { + delete state[data.button]; + var element = document.getElementById(data.button); + if (element) { + element.classList.remove('pressed'); + element.classList.add('released'); + } + } + } + } + + if (eventType == 2 && data.type == 2) { + if (data.pressed) { + state[data.button] = true; + var element = document.getElementById(data.button); + if (element) { + element.style.fill = '#2DA026'; + element.style.filter = 'url(#glow)'; + removeSvgReleased(element); + } + } else { + if (state[data.button]) { + delete state[data.button]; + var element = document.getElementById(data.button); + if (element) { + element.style.fill = '#96CF13'; + element.style.filter = 'none'; + addSvgReleased(element); + } + } + } + } + }; + }; +})(); diff --git a/wwwroot/index.html b/wwwroot/index.html new file mode 100644 index 0000000..81188dd --- /dev/null +++ b/wwwroot/index.html @@ -0,0 +1,196 @@ + + + + + + +
+
+
+
Esc
+
+
F1
+ + + +
+
+
+
~
+
1
+
2
+
3
+
4
+ + + + +
+
+
+
Tab
+
Q
+
W
+
E
+
R
+ + + + +
+
+
+
Caps
+
A
+
S
+
D
+
F
+
G
+ + + +
+
+
+
Shift
+
Z
+
X
+
C
+
V
+ + + +
+
+
+
Ctrl
+
+
Alt
+
Space
+
+
+
+ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/wwwroot/keyboard.html b/wwwroot/keyboard.html new file mode 100644 index 0000000..2aded06 --- /dev/null +++ b/wwwroot/keyboard.html @@ -0,0 +1,75 @@ + + + + + + +
+
+
Esc
+
+
F1
+ + + +
+
+
+
~
+
1
+
2
+
3
+
4
+ + + + +
+
+
+
Tab
+
Q
+
W
+
E
+
R
+ + + + +
+
+
+
Caps
+
A
+
S
+
D
+
F
+
G
+ + + +
+
+
+
Shift
+
Z
+
X
+
C
+
V
+ + + +
+
+
+
Ctrl
+
+
Alt
+
Space
+
+
+ + + diff --git a/wwwroot/mouse.html b/wwwroot/mouse.html new file mode 100644 index 0000000..4e2164a --- /dev/null +++ b/wwwroot/mouse.html @@ -0,0 +1,134 @@ + + + + + + +
+ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/wwwroot/mouse.svg b/wwwroot/mouse.svg new file mode 100644 index 0000000..c053f86 --- /dev/null +++ b/wwwroot/mouse.svg @@ -0,0 +1,129 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/wwwroot/styles.css b/wwwroot/styles.css new file mode 100644 index 0000000..2953db6 --- /dev/null +++ b/wwwroot/styles.css @@ -0,0 +1,111 @@ +html, body { + /*background-color: black;*/ + margin: 0; + padding: 0; + overflow: hidden; +} + +.hidden { + display: none !important; +} + +div.container { + display: block; + white-space: nowrap; +} + +div.keyboard { + display: inline-block; + margin: 10px; +} + +div.mouse { + display: inline-block; + margin: 10px; +} + +div.mouse > svg { + width: 200px; +} + +div.button, +div.small-button, +div.tab-button, +div.caps-button, +div.shift-button, +div.ctrl-button, +div.alt-button, +div.space-button { + border: 2px solid white; + border-radius: 10px; + color: white; + display: inline-block; + font-family: Calibri; + font-size: 20px; + line-height: 40px; + height: 40px; + text-align: center; + width: 40px; +} + +div.small-button { + line-height: 30px; + height: 30px; +} + +div.tab-button { + width: 60px; +} + +div.caps-button { + width: 70px; +} + +div.shift-button { + width: 90px; +} + +div.ctrl-button { + width: 60px; +} + +div.alt-button { + width: 60px; +} + +div.space-button { + width: 280px; +} + +div.pressed { + background-color: #2DA026; + box-shadow: 0 0 8px 3px rgba(35, 173, 278, 1); +} + +div.white-space { + border: 2px solid transparent; + display: inline-block; + width: 40px; +} + +div.line { + height: 5px; +} + +div.big-line { + height: 15px; +} + +div.released { + animation: key-released 0.5s ease-out; +} + +@keyframes key-released { + from { + background-color: #96CF13; + } + + to { + background-color: transparent; + } +} \ No newline at end of file