- streamer/windows: vash-broadcast.exe (setup/login/start/stop/status/doctor) with per-instance OBS, tosu and FFmpeg, a supervised worker, OCR room selection and OBS scene reconciliation; built and tested on Linux only - streamer/relay.ts shared with Vash TV; Cloudflare Access proxy for dev/staging - worker status validation and live score helpers for the site - STREAMING_HANDOFF.md and streamer/ALPHA_TEST.md record what is unverified Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
504 lines
21 KiB
PowerShell
504 lines
21 KiB
PowerShell
# Vash broadcast desktop helper.
|
|
#
|
|
# The worker starts this in the signed-in desktop session and sends one JSON
|
|
# request per line on stdin; each gets one JSON line back on stdout:
|
|
# {"id":1,"cmd":"windows","under":"C:\\...\\osu"} -> {"id":1,"ok":true,"result":[...]}
|
|
#
|
|
# It needs Windows PowerShell 5.1 (powershell.exe, part of Windows): the
|
|
# Windows.Media.Ocr API used for reading osu!tourney's room list is not
|
|
# reachable from PowerShell 7. Coordinates are physical screen pixels.
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
# Explicit UTF-8 streams: [Console]::InputEncoding cannot be set when there is no console window.
|
|
$utf8 = New-Object System.Text.UTF8Encoding $false
|
|
$stdin = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), $utf8)
|
|
$stdout = New-Object System.IO.StreamWriter([Console]::OpenStandardOutput(), $utf8)
|
|
$stdout.AutoFlush = $true
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @'
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Drawing.Imaging;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
public class VashWindow {
|
|
public long hwnd { get; set; }
|
|
public int pid { get; set; }
|
|
public string title { get; set; }
|
|
public string className { get; set; }
|
|
public string exe { get; set; }
|
|
public bool visible { get; set; }
|
|
public bool minimized { get; set; }
|
|
public int x { get; set; }
|
|
public int y { get; set; }
|
|
public int width { get; set; }
|
|
public int height { get; set; }
|
|
public int clientX { get; set; }
|
|
public int clientY { get; set; }
|
|
public int clientWidth { get; set; }
|
|
public int clientHeight { get; set; }
|
|
}
|
|
|
|
public class VashProcess {
|
|
public int pid { get; set; }
|
|
public string path { get; set; }
|
|
}
|
|
|
|
public static class VashDesktop {
|
|
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
|
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; }
|
|
[StructLayout(LayoutKind.Sequential)] struct MOUSEINPUT { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
|
[StructLayout(LayoutKind.Sequential)] struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
|
[StructLayout(LayoutKind.Sequential)] struct HARDWAREINPUT { public uint uMsg; public ushort wParamL; public ushort wParamH; }
|
|
[StructLayout(LayoutKind.Explicit)] struct InputUnion {
|
|
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
[FieldOffset(0)] public HARDWAREINPUT hi;
|
|
}
|
|
[StructLayout(LayoutKind.Sequential)] struct INPUT { public uint type; public InputUnion U; }
|
|
[StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }
|
|
|
|
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
|
|
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max);
|
|
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hWnd);
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max);
|
|
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
|
|
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr hWnd);
|
|
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr hWnd);
|
|
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
|
|
[DllImport("user32.dll")] static extern bool GetClientRect(IntPtr hWnd, out RECT r);
|
|
[DllImport("user32.dll")] static extern bool ClientToScreen(IntPtr hWnd, ref POINT p);
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, IntPtr w, string l, uint flags, uint timeout, out IntPtr result);
|
|
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
|
|
[DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
|
[DllImport("user32.dll")] static extern bool BringWindowToTop(IntPtr hWnd);
|
|
[DllImport("user32.dll")] static extern bool AttachThreadInput(uint a, uint b, bool attach);
|
|
[DllImport("kernel32.dll")] static extern uint GetCurrentThreadId();
|
|
[DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y);
|
|
[DllImport("user32.dll")] static extern uint SendInput(uint n, INPUT[] inputs, int size);
|
|
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
|
[DllImport("user32.dll")] static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr w, IntPtr l);
|
|
[DllImport("user32.dll")] static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
|
|
[DllImport("user32.dll")] static extern bool CloseDesktop(IntPtr h);
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool GetUserObjectInformation(IntPtr h, int index, StringBuilder sb, int len, out int needed);
|
|
[DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr value);
|
|
[DllImport("user32.dll")] static extern int GetSystemMetrics(int index);
|
|
[DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO info);
|
|
[DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern bool QueryFullProcessImageName(IntPtr h, uint flags, StringBuilder sb, ref uint size);
|
|
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
|
|
[DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId();
|
|
[DllImport("kernel32.dll")] static extern bool ProcessIdToSessionId(uint pid, out uint session);
|
|
[DllImport("kernel32.dll")] static extern uint SetThreadExecutionState(uint flags);
|
|
|
|
/** Keeps the PC awake and the display on while this helper runs (it reverts when the helper exits). */
|
|
public static bool StayAwake(bool on) {
|
|
// ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
|
uint flags = on ? 0x80000000u | 0x00000001u | 0x00000002u : 0x80000000u;
|
|
return SetThreadExecutionState(flags) != 0;
|
|
}
|
|
|
|
public static string Init() {
|
|
try {
|
|
// Per-monitor aware v2: every rectangle and click is in physical pixels.
|
|
if (SetProcessDpiAwarenessContext(new IntPtr(-4))) return "per-monitor-v2";
|
|
return "unchanged";
|
|
} catch (Exception e) {
|
|
return "unavailable: " + e.Message;
|
|
}
|
|
}
|
|
|
|
public static string ProcessPath(uint pid) {
|
|
IntPtr h = OpenProcess(0x1000, false, pid);
|
|
if (h == IntPtr.Zero) return null;
|
|
try {
|
|
var sb = new StringBuilder(1024);
|
|
uint size = (uint)sb.Capacity;
|
|
return QueryFullProcessImageName(h, 0, sb, ref size) ? sb.ToString() : null;
|
|
} finally {
|
|
CloseHandle(h);
|
|
}
|
|
}
|
|
|
|
static bool Under(string path, string dir) {
|
|
if (String.IsNullOrEmpty(dir)) return true;
|
|
if (path == null) return false;
|
|
string prefix = dir.TrimEnd('\\') + "\\";
|
|
return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public static VashWindow Describe(IntPtr h) {
|
|
uint pid;
|
|
GetWindowThreadProcessId(h, out pid);
|
|
var title = new StringBuilder(GetWindowTextLength(h) + 1);
|
|
GetWindowText(h, title, title.Capacity);
|
|
var cls = new StringBuilder(256);
|
|
GetClassName(h, cls, cls.Capacity);
|
|
RECT r, c;
|
|
GetWindowRect(h, out r);
|
|
GetClientRect(h, out c);
|
|
var origin = new POINT();
|
|
ClientToScreen(h, ref origin);
|
|
string path = ProcessPath(pid);
|
|
return new VashWindow {
|
|
hwnd = h.ToInt64(), pid = (int)pid, title = title.ToString(), className = cls.ToString(),
|
|
exe = path, visible = IsWindowVisible(h), minimized = IsIconic(h),
|
|
x = r.Left, y = r.Top, width = r.Right - r.Left, height = r.Bottom - r.Top,
|
|
clientX = origin.X, clientY = origin.Y, clientWidth = c.Right - c.Left, clientHeight = c.Bottom - c.Top
|
|
};
|
|
}
|
|
|
|
/** Top-level windows of processes whose executable is under dir (all when empty). */
|
|
public static VashWindow[] Windows(string dir) {
|
|
var handles = new List<IntPtr>();
|
|
EnumWindows((h, l) => { handles.Add(h); return true; }, IntPtr.Zero);
|
|
var result = new List<VashWindow>();
|
|
var paths = new Dictionary<uint, string>();
|
|
foreach (var h in handles) {
|
|
uint pid;
|
|
GetWindowThreadProcessId(h, out pid);
|
|
string path;
|
|
if (!paths.TryGetValue(pid, out path)) { path = ProcessPath(pid); paths[pid] = path; }
|
|
if (!Under(path, dir)) continue;
|
|
result.Add(Describe(h));
|
|
}
|
|
return result.ToArray();
|
|
}
|
|
|
|
public static VashWindow Window(long hwnd) {
|
|
var h = new IntPtr(hwnd);
|
|
return IsWindow(h) ? Describe(h) : null;
|
|
}
|
|
|
|
public static VashProcess[] Processes(string dir) {
|
|
var result = new List<VashProcess>();
|
|
foreach (var p in Process.GetProcesses()) {
|
|
try {
|
|
string path = ProcessPath((uint)p.Id);
|
|
if (path != null && Under(path, dir)) result.Add(new VashProcess { pid = p.Id, path = path });
|
|
} catch { }
|
|
finally { p.Dispose(); }
|
|
}
|
|
return result.ToArray();
|
|
}
|
|
|
|
public static int Kill(int[] pids) {
|
|
int killed = 0;
|
|
foreach (int pid in pids) {
|
|
try { using (var p = Process.GetProcessById(pid)) { p.Kill(); killed++; } } catch { }
|
|
}
|
|
return killed;
|
|
}
|
|
|
|
/** Asks every top-level window of a process to close, like clicking its X. */
|
|
public static int CloseProcessWindows(int pid) {
|
|
int posted = 0;
|
|
foreach (var w in Windows(null)) {
|
|
if (w.pid != pid) continue;
|
|
if (PostMessage(new IntPtr(w.hwnd), 0x0010, IntPtr.Zero, IntPtr.Zero)) posted++;
|
|
}
|
|
return posted;
|
|
}
|
|
|
|
/** "Default" when the desktop is unlocked; "Winlogon" (or null) when locked or on a secure screen. */
|
|
public static string InputDesktop() {
|
|
IntPtr d = OpenInputDesktop(0, false, 0x0001);
|
|
if (d == IntPtr.Zero) return null;
|
|
try {
|
|
var sb = new StringBuilder(256);
|
|
int needed;
|
|
return GetUserObjectInformation(d, 2, sb, sb.Capacity * 2, out needed) ? sb.ToString() : null;
|
|
} finally {
|
|
CloseDesktop(d);
|
|
}
|
|
}
|
|
|
|
public static Dictionary<string, object> Session() {
|
|
uint session;
|
|
ProcessIdToSessionId((uint)Process.GetCurrentProcess().Id, out session);
|
|
var info = new LASTINPUTINFO { cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO)) };
|
|
GetLastInputInfo(ref info);
|
|
return new Dictionary<string, object> {
|
|
{ "sessionId", (int)session },
|
|
{ "consoleSessionId", (int)WTSGetActiveConsoleSessionId() },
|
|
{ "inputDesktop", InputDesktop() },
|
|
{ "screenWidth", GetSystemMetrics(0) },
|
|
{ "screenHeight", GetSystemMetrics(1) },
|
|
{ "idleMs", unchecked(Environment.TickCount - (int)info.dwTime) }
|
|
};
|
|
}
|
|
|
|
static void Key(ushort vk, bool up) {
|
|
var input = new INPUT { type = 1 };
|
|
input.U.ki.wVk = vk;
|
|
input.U.ki.dwFlags = up ? 0x0002u : 0u;
|
|
SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT)));
|
|
}
|
|
|
|
/** Brings a window to the front; Windows only allows this in some cases, so try the known ways. */
|
|
public static bool Focus(long hwnd) {
|
|
var h = new IntPtr(hwnd);
|
|
if (!IsWindow(h)) return false;
|
|
if (IsIconic(h)) ShowWindow(h, 9);
|
|
if (GetForegroundWindow() == h) return true;
|
|
IntPtr fg = GetForegroundWindow();
|
|
uint ignored;
|
|
uint fgThread = fg == IntPtr.Zero ? 0 : GetWindowThreadProcessId(fg, out ignored);
|
|
uint me = GetCurrentThreadId();
|
|
bool attached = fgThread != 0 && fgThread != me && AttachThreadInput(me, fgThread, true);
|
|
BringWindowToTop(h);
|
|
SetForegroundWindow(h);
|
|
if (attached) AttachThreadInput(me, fgThread, false);
|
|
Thread.Sleep(80);
|
|
if (GetForegroundWindow() == h) return true;
|
|
// The process that sent the last input may set the foreground window: tap Alt, then retry.
|
|
Key(0x12, false);
|
|
Key(0x12, true);
|
|
SetForegroundWindow(h);
|
|
Thread.Sleep(120);
|
|
return GetForegroundWindow() == h;
|
|
}
|
|
|
|
public static bool Move(long hwnd, int x, int y) {
|
|
// SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE
|
|
return SetWindowPos(new IntPtr(hwnd), IntPtr.Zero, x, y, 0, 0, 0x0001 | 0x0004 | 0x0010);
|
|
}
|
|
|
|
public static bool SetTitle(long hwnd, string title) {
|
|
IntPtr result;
|
|
// WM_SETTEXT with a timeout, so a busy window cannot hang the helper.
|
|
return SendMessageTimeout(new IntPtr(hwnd), 0x000C, IntPtr.Zero, title, 0x0002, 2000, out result) != IntPtr.Zero;
|
|
}
|
|
|
|
static ImageCodecInfo Codec(string mime) {
|
|
foreach (var codec in ImageCodecInfo.GetImageEncoders()) if (codec.MimeType == mime) return codec;
|
|
return null;
|
|
}
|
|
|
|
static void Save(Bitmap bmp, string path, string format, long quality) {
|
|
if (format == "jpeg") {
|
|
var parameters = new EncoderParameters(1);
|
|
parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
|
|
bmp.Save(path, Codec("image/jpeg"), parameters);
|
|
} else {
|
|
bmp.Save(path, ImageFormat.Png);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies a screen rectangle to an image file, optionally scaled (to at most maxWidth,
|
|
* or by scale) and inverted (dark text on light reads better for OCR). Returns
|
|
* the saved size. Fails while the desktop is locked.
|
|
*/
|
|
public static int[] Capture(int x, int y, int w, int h, string path, string format, int maxWidth, double scale, bool invert, long quality) {
|
|
using (var bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb)) {
|
|
using (var g = Graphics.FromImage(bmp)) {
|
|
g.CopyFromScreen(x, y, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
|
|
}
|
|
double factor = scale > 0 ? scale : 1.0;
|
|
if (maxWidth > 0 && w * factor > maxWidth) factor = (double)maxWidth / w;
|
|
int ow = Math.Max(1, (int)Math.Round(w * factor));
|
|
int oh = Math.Max(1, (int)Math.Round(h * factor));
|
|
if (ow == w && oh == h && !invert) {
|
|
Save(bmp, path, format, quality);
|
|
return new[] { ow, oh };
|
|
}
|
|
using (var output = new Bitmap(ow, oh, PixelFormat.Format24bppRgb)) {
|
|
using (var g = Graphics.FromImage(output)) {
|
|
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
var attributes = new ImageAttributes();
|
|
if (invert) {
|
|
attributes.SetColorMatrix(new ColorMatrix(new float[][] {
|
|
new float[] { -1, 0, 0, 0, 0 },
|
|
new float[] { 0, -1, 0, 0, 0 },
|
|
new float[] { 0, 0, -1, 0, 0 },
|
|
new float[] { 0, 0, 0, 1, 0 },
|
|
new float[] { 1, 1, 1, 0, 1 }
|
|
}));
|
|
}
|
|
g.DrawImage(bmp, new Rectangle(0, 0, ow, oh), 0, 0, w, h, GraphicsUnit.Pixel, attributes);
|
|
}
|
|
Save(output, path, format, quality);
|
|
}
|
|
return new[] { ow, oh };
|
|
}
|
|
}
|
|
|
|
public static bool Click(int x, int y, bool right) {
|
|
if (!SetCursorPos(x, y)) return false;
|
|
Thread.Sleep(60);
|
|
var down = new INPUT { type = 0 };
|
|
down.U.mi.dwFlags = right ? 0x0008u : 0x0002u;
|
|
var up = new INPUT { type = 0 };
|
|
up.U.mi.dwFlags = right ? 0x0010u : 0x0004u;
|
|
uint sent = SendInput(1, new[] { down }, Marshal.SizeOf(typeof(INPUT)));
|
|
Thread.Sleep(60);
|
|
sent += SendInput(1, new[] { up }, Marshal.SizeOf(typeof(INPUT)));
|
|
return sent == 2;
|
|
}
|
|
|
|
public static int TypeText(string text) {
|
|
int sent = 0;
|
|
foreach (char ch in text) {
|
|
var down = new INPUT { type = 1 };
|
|
down.U.ki.wScan = ch;
|
|
down.U.ki.dwFlags = 0x0004;
|
|
var up = new INPUT { type = 1 };
|
|
up.U.ki.wScan = ch;
|
|
up.U.ki.dwFlags = 0x0004 | 0x0002;
|
|
sent += (int)SendInput(1, new[] { down }, Marshal.SizeOf(typeof(INPUT)));
|
|
sent += (int)SendInput(1, new[] { up }, Marshal.SizeOf(typeof(INPUT)));
|
|
Thread.Sleep(15);
|
|
}
|
|
return sent / 2;
|
|
}
|
|
|
|
public static void PressKey(ushort vk) {
|
|
Key(vk, false);
|
|
Thread.Sleep(30);
|
|
Key(vk, true);
|
|
}
|
|
}
|
|
'@
|
|
|
|
$script:dpi = [VashDesktop]::Init()
|
|
|
|
# ── Windows OCR (Windows.Media.Ocr, part of Windows 10 and 11) ─────────────
|
|
|
|
$script:ocr = $null
|
|
function Initialize-Ocr {
|
|
if ($null -ne $script:ocr) { return }
|
|
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
$null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
|
|
$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
$null = [Windows.Foundation.IAsyncOperation`1, Windows.Foundation, ContentType = WindowsRuntime]
|
|
$null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime]
|
|
$null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
|
|
$null = [Windows.Storage.Streams.RandomAccessStream, Windows.Storage.Streams, ContentType = WindowsRuntime]
|
|
$script:asTask = [System.WindowsRuntimeSystemExtensions].GetMethods() |
|
|
Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } |
|
|
Select-Object -First 1
|
|
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
|
if ($null -eq $engine) {
|
|
try {
|
|
$null = [Windows.Globalization.Language, Windows.Globalization, ContentType = WindowsRuntime]
|
|
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new('en-US'))
|
|
} catch { }
|
|
}
|
|
if ($null -eq $engine) {
|
|
throw 'Windows OCR has no language available. Add English under Settings > Time & language > Language & region.'
|
|
}
|
|
$script:ocr = $engine
|
|
}
|
|
|
|
function Await($operation, [Type]$type) {
|
|
$task = $script:asTask.MakeGenericMethod($type).Invoke($null, @($operation))
|
|
if (-not $task.Wait(20000)) { throw 'Windows OCR timed out' }
|
|
$task.Result
|
|
}
|
|
|
|
function Read-Text([string]$path) {
|
|
Initialize-Ocr
|
|
$file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($path)) ([Windows.Storage.StorageFile])
|
|
$stream = Await ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream])
|
|
try {
|
|
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
|
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
|
$result = Await ($script:ocr.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
|
} finally {
|
|
$stream.Dispose()
|
|
}
|
|
$lines = New-Object System.Collections.ArrayList
|
|
foreach ($line in $result.Lines) {
|
|
$left = [double]::MaxValue; $top = [double]::MaxValue; $right = 0.0; $bottom = 0.0
|
|
foreach ($word in $line.Words) {
|
|
$r = $word.BoundingRect
|
|
$left = [Math]::Min($left, $r.X); $top = [Math]::Min($top, $r.Y)
|
|
$right = [Math]::Max($right, $r.X + $r.Width); $bottom = [Math]::Max($bottom, $r.Y + $r.Height)
|
|
}
|
|
if ($right -le 0) { continue }
|
|
$null = $lines.Add(@{ text = $line.Text; x = $left; y = $top; width = $right - $left; height = $bottom - $top })
|
|
}
|
|
return , $lines.ToArray()
|
|
}
|
|
|
|
# ── Request loop ──────────────────────────────────────────────────────────
|
|
|
|
function Reply($id, [bool]$ok, $value) {
|
|
$message = @{ id = $id; ok = $ok }
|
|
if ($ok) { $message.result = $value } else { $message.error = [string]$value }
|
|
$stdout.WriteLine((ConvertTo-Json -InputObject $message -Compress -Depth 6))
|
|
}
|
|
|
|
Reply 0 $true @{ ready = $true; dpi = $script:dpi; powershell = $PSVersionTable.PSVersion.ToString() }
|
|
|
|
while ($true) {
|
|
$line = $stdin.ReadLine()
|
|
if ($null -eq $line) { break }
|
|
if (-not $line.Trim()) { continue }
|
|
$id = $null
|
|
try {
|
|
$request = ConvertFrom-Json $line
|
|
$id = $request.id
|
|
$cmd = [string]$request.cmd
|
|
$result = $null
|
|
if ($cmd -eq 'ping') {
|
|
$result = @{ pong = $true }
|
|
} elseif ($cmd -eq 'awake') {
|
|
$result = [VashDesktop]::StayAwake([bool]$request.on)
|
|
} elseif ($cmd -eq 'session') {
|
|
$result = [VashDesktop]::Session()
|
|
} elseif ($cmd -eq 'windows') {
|
|
$result = [VashDesktop]::Windows([string]$request.under)
|
|
} elseif ($cmd -eq 'window') {
|
|
$result = [VashDesktop]::Window([long]$request.hwnd)
|
|
} elseif ($cmd -eq 'processes') {
|
|
$result = [VashDesktop]::Processes([string]$request.under)
|
|
} elseif ($cmd -eq 'kill') {
|
|
$result = [VashDesktop]::Kill([int[]]@($request.pids))
|
|
} elseif ($cmd -eq 'close') {
|
|
$result = [VashDesktop]::CloseProcessWindows([int]$request.pid)
|
|
} elseif ($cmd -eq 'focus') {
|
|
$result = [VashDesktop]::Focus([long]$request.hwnd)
|
|
} elseif ($cmd -eq 'move') {
|
|
$result = [VashDesktop]::Move([long]$request.hwnd, [int]$request.x, [int]$request.y)
|
|
} elseif ($cmd -eq 'title') {
|
|
$result = [VashDesktop]::SetTitle([long]$request.hwnd, [string]$request.title)
|
|
} elseif ($cmd -eq 'capture') {
|
|
$format = if ($request.format) { [string]$request.format } else { 'png' }
|
|
$quality = if ($request.quality) { [long]$request.quality } else { 80 }
|
|
$size = [VashDesktop]::Capture([int]$request.x, [int]$request.y, [int]$request.width, [int]$request.height,
|
|
[string]$request.path, $format, [int]$request.maxWidth, [double]$request.scale, [bool]$request.invert, $quality)
|
|
$result = @{ width = $size[0]; height = $size[1] }
|
|
} elseif ($cmd -eq 'ocr') {
|
|
$result = Read-Text ([string]$request.path)
|
|
} elseif ($cmd -eq 'ocrReady') {
|
|
Initialize-Ocr
|
|
$result = @{ language = $script:ocr.RecognizerLanguage.LanguageTag; maxDimension = [Windows.Media.Ocr.OcrEngine]::MaxImageDimension }
|
|
} elseif ($cmd -eq 'click') {
|
|
$result = [VashDesktop]::Click([int]$request.x, [int]$request.y, [bool]$request.right)
|
|
} elseif ($cmd -eq 'type') {
|
|
$result = [VashDesktop]::TypeText([string]$request.text)
|
|
} elseif ($cmd -eq 'key') {
|
|
[VashDesktop]::PressKey([uint16]$request.vk)
|
|
$result = $true
|
|
} else {
|
|
throw "Unknown command $cmd"
|
|
}
|
|
Reply $id $true $result
|
|
} catch {
|
|
Reply $id $false $_.Exception.Message
|
|
}
|
|
}
|