@y11i-3d/chrome-recording 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,13 +25,14 @@ Or run directly with `npx`:
25
25
  npx @y11i-3d/chrome-recording <subcommand> [options]
26
26
  ```
27
27
 
28
- After installation, a PowerShell script (`get_chrome_info.ps1`) is automatically copied to `%LOCALAPPDATA%\y11i-3d\chrome-recording\` on Windows. This script is required by the `record` command.
28
+ After installation, a PowerShell script (`chrome.ps1`) is automatically copied to `%LOCALAPPDATA%\y11i-3d\chrome-recording\` on Windows. This script is required by the `record` and `resize` commands.
29
29
 
30
30
  ## Subcommands
31
31
 
32
32
  | Subcommand | Description |
33
33
  | ---------- | ------------------------------------------------------- |
34
34
  | `record` | Capture Chrome browser content as a video or screenshot |
35
+ | `resize` | Set the Chrome viewport size |
35
36
  | `concat` | Concatenate recorded mp4 files into one |
36
37
 
37
38
  ## record
@@ -53,6 +54,8 @@ By default, recording starts immediately and stops when you press Enter. The out
53
54
  | `-d, --duration <seconds>` | Stop recording automatically after N seconds |
54
55
  | `-q, --quality <value>` | Quality: plain number for CRF (e.g. `23`), number+unit for bitrate (e.g. `4000k`, `4M`) |
55
56
  | `-f, --fps <fps>` | Frame rate |
57
+ | `-n, --no-pointer` | Hide the mouse pointer |
58
+ | `-t, --title <text>` | Select a window whose page title contains this text |
56
59
  | `-v, --verbose` | Show ffmpeg output |
57
60
 
58
61
  ### Examples
@@ -74,6 +77,41 @@ chrome-recording record -m -c 40:0:0:0
74
77
  chrome-recording record -o output.mp4 -q 4M
75
78
  ```
76
79
 
80
+ ## resize
81
+
82
+ Set the viewport size through the Chrome DevTools Protocol. Chrome asks you to
83
+ approve the connection each time this command runs.
84
+
85
+ ```bash
86
+ chrome-recording resize <width>x<height> [options]
87
+ ```
88
+
89
+ The viewport is the area the page is rendered into, and it is what `record`
90
+ captures. `--padding` enlarges the surrounding content area without changing the
91
+ viewport, which keeps the rounded window corners away from the captured region.
92
+ Padding is added to the right and the bottom.
93
+
94
+ | Option | Description |
95
+ | -------------------------------- | -------------------------------------------------- |
96
+ | `-p, --padding <width>x<height>` | Padding kept outside the viewport (default: `0x0`) |
97
+ | `-t, --title <text>` | Select a page whose title contains this text |
98
+
99
+ The setting survives after the command exits, but it is lost when the page is
100
+ reloaded.
101
+
102
+ ### Examples
103
+
104
+ ```bash
105
+ # Set the viewport to 1920x1080
106
+ chrome-recording resize 1920x1080
107
+
108
+ # Keep 4px below the viewport so the rounded corners are not captured
109
+ chrome-recording resize 1920x1080 -p 0x4
110
+
111
+ # Target a specific page
112
+ chrome-recording resize 1280x720 -t "Example"
113
+ ```
114
+
77
115
  ## concat
78
116
 
79
117
  Concatenate all mp4 files in a directory into a single file (stream copy, no re-encode).
@@ -0,0 +1,5 @@
1
+ export declare function runChromeScript(args: string[]): string[];
2
+ export declare function parseSize(value: string, label: string): {
3
+ width: number;
4
+ height: number;
5
+ };
package/dist/chrome.js ADDED
@@ -0,0 +1,33 @@
1
+ import { execSync } from "child_process";
2
+ const SCRIPT_NAME = "chrome.ps1";
3
+ function scriptPathWin() {
4
+ const localAppDataWin = execSync("powershell.exe -Command '$env:LOCALAPPDATA'")
5
+ .toString()
6
+ .trim();
7
+ return `${localAppDataWin}\\y11i-3d\\chrome-recording\\${SCRIPT_NAME}`;
8
+ }
9
+ export function runChromeScript(args) {
10
+ const quoted = args.map((arg) => `"${arg.replace(/"/g, '""')}"`).join(" ");
11
+ const command = `powershell.exe -ExecutionPolicy Bypass -File "${scriptPathWin()}" ${quoted}`;
12
+ try {
13
+ return execSync(command, { stdio: ["ignore", "pipe", "pipe"] })
14
+ .toString()
15
+ .trim()
16
+ .split("\n")
17
+ .map((line) => line.trim().replace(/\r/, ""))
18
+ .filter((line) => line.length > 0);
19
+ }
20
+ catch (error) {
21
+ const stderr = error.stderr?.toString().trim();
22
+ throw new Error(stderr && stderr.length > 0 ? stderr : String(error), {
23
+ cause: error,
24
+ });
25
+ }
26
+ }
27
+ export function parseSize(value, label) {
28
+ const match = /^(\d+)x(\d+)$/.exec(value);
29
+ if (!match) {
30
+ throw new Error(`Invalid ${label}: "${value}". Expected <width>x<height>.`);
31
+ }
32
+ return { width: Number(match[1]), height: Number(match[2]) };
33
+ }
@@ -0,0 +1,212 @@
1
+ param(
2
+ [Parameter(Position = 0)][ValidateSet("info", "resize")][string]$Command = "info",
3
+ [string]$Title = "",
4
+ [int]$ViewportWidth = 0,
5
+ [int]$ViewportHeight = 0,
6
+ [int]$PaddingWidth = 0,
7
+ [int]$PaddingHeight = 0
8
+ )
9
+
10
+ $ErrorActionPreference = "Stop"
11
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
12
+ Add-Type -AssemblyName System.Windows.Forms
13
+
14
+ Add-Type -TypeDefinition @"
15
+ using System;
16
+ using System.Collections.Generic;
17
+ using System.Runtime.InteropServices;
18
+ using System.Text;
19
+ public class Win32 {
20
+ public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
21
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
22
+ [DllImport("user32.dll")] public static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc cb, IntPtr lParam);
23
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
24
+ [DllImport("user32.dll")] public static extern int GetClassName(IntPtr hwnd, StringBuilder buf, int maxCount);
25
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr hwnd, StringBuilder buf, int maxCount);
26
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
27
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
28
+ public struct RECT { public int Left, Top, Right, Bottom; }
29
+
30
+ public const string TitleSuffix = " - Google Chrome";
31
+
32
+ public static List<IntPtr> FindBrowserWindows(uint[] pids, string query) {
33
+ var set = new HashSet<uint>(pids);
34
+ var results = new List<IntPtr>();
35
+ EnumWindows((h, _) => {
36
+ uint pid;
37
+ GetWindowThreadProcessId(h, out pid);
38
+ if (!set.Contains(pid)) return true;
39
+ var cls = new StringBuilder(256);
40
+ GetClassName(h, cls, 256);
41
+ if (cls.ToString() != "Chrome_WidgetWin_1") return true;
42
+ if (!IsWindowVisible(h)) return true;
43
+ var buf = new StringBuilder(512);
44
+ GetWindowTextW(h, buf, 512);
45
+ var title = buf.ToString();
46
+ if (!title.EndsWith(TitleSuffix)) return true;
47
+ if (query.Length > 0) {
48
+ var pageTitle = title.Substring(0, title.Length - TitleSuffix.Length);
49
+ if (pageTitle.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0) return true;
50
+ }
51
+ results.Add(h);
52
+ return true;
53
+ }, IntPtr.Zero);
54
+ return results;
55
+ }
56
+
57
+ public static List<RECT> FindContentRects(IntPtr chromeHwnd) {
58
+ RECT mainRect;
59
+ GetWindowRect(chromeHwnd, out mainRect);
60
+ var results = new List<RECT>();
61
+ EnumChildWindows(chromeHwnd, (h, _) => {
62
+ var buf = new StringBuilder(256);
63
+ GetClassName(h, buf, 256);
64
+ if (buf.ToString() == "Chrome_RenderWidgetHostHWND" && IsWindowVisible(h)) {
65
+ RECT r;
66
+ GetWindowRect(h, out r);
67
+ if (r.Top >= mainRect.Top && r.Top < mainRect.Bottom) {
68
+ results.Add(r);
69
+ }
70
+ }
71
+ return true;
72
+ }, IntPtr.Zero);
73
+ return results;
74
+ }
75
+ }
76
+ "@
77
+
78
+ $chromePids = @(Get-Process chrome -ErrorAction SilentlyContinue | ForEach-Object { [uint32]$_.Id })
79
+ if ($chromePids.Count -eq 0) {
80
+ [Console]::Error.WriteLine("Chrome is not running.")
81
+ exit 1
82
+ }
83
+
84
+ function Get-TargetWindow {
85
+ $windows = [Win32]::FindBrowserWindows($chromePids, $Title)
86
+ if ($windows.Count -eq 0) {
87
+ if ($Title.Length -gt 0) {
88
+ [Console]::Error.WriteLine("No Chrome window matching '$Title' was found.")
89
+ }
90
+ else {
91
+ [Console]::Error.WriteLine("Chrome is not running or has no visible window.")
92
+ }
93
+ exit 1
94
+ }
95
+ return $windows[0]
96
+ }
97
+
98
+ if ($Command -eq "info") {
99
+ $hwnd = Get-TargetWindow
100
+ $rects = [Win32]::FindContentRects($hwnd)
101
+ $best = $rects | Sort-Object { $_.Left }, { $_.Top } | Select-Object -First 1
102
+ if (-not $best) {
103
+ [Console]::Error.WriteLine("Chrome content area (Chrome_RenderWidgetHostHWND) not found.")
104
+ exit 1
105
+ }
106
+
107
+ $x = $best.Left
108
+ $y = $best.Top
109
+ $w = $best.Right - $best.Left
110
+ $h = $best.Bottom - $best.Top
111
+ Write-Output "content $x $y $w $h"
112
+
113
+ $i = 0
114
+ foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
115
+ $b = $s.Bounds
116
+ Write-Output "monitor $i $($b.Left) $($b.Top) $($b.Right) $($b.Bottom)"
117
+ $i++
118
+ }
119
+ exit 0
120
+ }
121
+
122
+ if ([Win32]::FindBrowserWindows($chromePids, "").Count -eq 0) {
123
+ [Console]::Error.WriteLine("Chrome is not running or has no visible window.")
124
+ exit 1
125
+ }
126
+
127
+ $portFile = Join-Path $env:LOCALAPPDATA "Google\Chrome\User Data\DevToolsActivePort"
128
+ if (-not (Test-Path $portFile)) {
129
+ [Console]::Error.WriteLine("DevToolsActivePort not found. Enable remote debugging at chrome://inspect/#remote-debugging.")
130
+ exit 1
131
+ }
132
+ $portLines = Get-Content $portFile
133
+ $endpoint = "ws://127.0.0.1:$($portLines[0].Trim())$($portLines[1].Trim())"
134
+
135
+ $ws = New-Object System.Net.WebSockets.ClientWebSocket
136
+ $token = [System.Threading.CancellationToken]::None
137
+ $connected = $false
138
+ try {
139
+ $connected = $ws.ConnectAsync([Uri]$endpoint, $token).Wait(60000)
140
+ }
141
+ catch {
142
+ $connected = $false
143
+ }
144
+ if (-not $connected) {
145
+ [Console]::Error.WriteLine("Could not connect to Chrome. Approve the connection prompt in the browser and try again.")
146
+ exit 1
147
+ }
148
+
149
+ $script:cdpId = 0
150
+ function Invoke-Cdp([string]$method, $params, [string]$sessionId) {
151
+ $script:cdpId++
152
+ $myId = $script:cdpId
153
+ $payload = [ordered]@{ id = $myId; method = $method; params = if ($params) { $params } else { @{} } }
154
+ if ($sessionId) { $payload.sessionId = $sessionId }
155
+ $json = (New-Object psobject -Property $payload) | ConvertTo-Json -Depth 10 -Compress
156
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
157
+ $segment = New-Object System.ArraySegment[byte] -ArgumentList @(, $bytes)
158
+ $ws.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $token).Wait(10000) | Out-Null
159
+
160
+ for ($i = 0; $i -lt 500; $i++) {
161
+ $sb = New-Object System.Text.StringBuilder
162
+ do {
163
+ $buf = New-Object byte[] 65536
164
+ $recvSegment = New-Object System.ArraySegment[byte] -ArgumentList @(, $buf)
165
+ $recv = $ws.ReceiveAsync($recvSegment, $token)
166
+ if (-not $recv.Wait(10000)) { throw "Timed out waiting for a CDP response." }
167
+ [void]$sb.Append([System.Text.Encoding]::UTF8.GetString($buf, 0, $recv.Result.Count))
168
+ } while (-not $recv.Result.EndOfMessage)
169
+ $message = $sb.ToString() | ConvertFrom-Json
170
+ if ($message.id -eq $myId) {
171
+ if ($message.error) { throw "CDP error on $method : $($message.error.message)" }
172
+ return $message
173
+ }
174
+ }
175
+ throw "No CDP response for $method."
176
+ }
177
+
178
+ try {
179
+ $targets = (Invoke-Cdp "Target.getTargets" $null $null).result.targetInfos
180
+ $pages = @($targets | Where-Object { $_.type -eq "page" })
181
+ if ($Title.Length -gt 0) {
182
+ $pages = @($pages | Where-Object { $_.title -like "*$Title*" })
183
+ }
184
+ if ($pages.Count -eq 0) {
185
+ if ($Title.Length -gt 0) { throw "No page matching '$Title' was found." }
186
+ throw "No page was found."
187
+ }
188
+ $target = $pages[0]
189
+
190
+ $windowId = (Invoke-Cdp "Browser.getWindowForTarget" @{ targetId = $target.targetId } $null).result.windowId
191
+ $contentWidth = $ViewportWidth + $PaddingWidth
192
+ $contentHeight = $ViewportHeight + $PaddingHeight
193
+ Invoke-Cdp "Browser.setContentsSize" @{ windowId = $windowId; width = $contentWidth; height = $contentHeight } $null | Out-Null
194
+
195
+ $sessionId = (Invoke-Cdp "Target.attachToTarget" @{ targetId = $target.targetId; flatten = $true } $null).result.sessionId
196
+ Invoke-Cdp "Emulation.setDeviceMetricsOverride" @{
197
+ width = $ViewportWidth
198
+ height = $ViewportHeight
199
+ deviceScaleFactor = 1
200
+ mobile = $false
201
+ } $sessionId | Out-Null
202
+
203
+ Write-Output "viewport $ViewportWidth $ViewportHeight"
204
+ Write-Output "content $contentWidth $contentHeight"
205
+ Write-Output "title $($target.title)"
206
+ }
207
+ catch {
208
+ [Console]::Error.WriteLine($_.Exception.Message)
209
+ $ws.Dispose()
210
+ exit 1
211
+ }
212
+ $ws.Dispose()
package/dist/cli.js CHANGED
@@ -2,10 +2,18 @@
2
2
  import { Command } from "commander";
3
3
  import { registerConcatCommand } from "./concat.js";
4
4
  import { registerRecordCommand } from "./record.js";
5
+ import { registerResizeCommand } from "./resize.js";
6
+ function reportAndExit(error) {
7
+ console.error(error instanceof Error ? error.message : String(error));
8
+ process.exit(1);
9
+ }
10
+ process.on("uncaughtException", reportAndExit);
11
+ process.on("unhandledRejection", reportAndExit);
5
12
  const program = new Command();
6
13
  program
7
14
  .name("chrome-recording")
8
15
  .description("Record Chrome browser content in WSL");
9
16
  registerRecordCommand(program);
17
+ registerResizeCommand(program);
10
18
  registerConcatCommand(program);
11
19
  program.parse();
@@ -6,7 +6,7 @@ import { execSync } from "child_process";
6
6
  import pkg from "../package.json" with { type: "json" };
7
7
  const [scope, pkgName] = pkg.name.replace(/^@/, "").split("/");
8
8
  const scriptDir = dirname(fileURLToPath(import.meta.url));
9
- const ps1Src = resolve(scriptDir, "get_chrome_info.ps1");
9
+ const ps1Src = resolve(scriptDir, "chrome.ps1");
10
10
  const localAppDataWin = execSync("powershell.exe -Command '$env:LOCALAPPDATA'")
11
11
  .toString()
12
12
  .trim();
@@ -15,5 +15,5 @@ const localAppDataLinux = execSync(`wslpath -u "${localAppDataWin}"`)
15
15
  .trim();
16
16
  const destDir = resolve(localAppDataLinux, scope ?? pkg.name, pkgName ?? "");
17
17
  mkdirSync(destDir, { recursive: true });
18
- copyFileSync(ps1Src, resolve(destDir, "get_chrome_info.ps1"));
19
- console.log(`Installed get_chrome_info.ps1 to ${destDir}`);
18
+ copyFileSync(ps1Src, resolve(destDir, "chrome.ps1"));
19
+ console.log(`Installed chrome.ps1 to ${destDir}`);
package/dist/record.js CHANGED
@@ -2,6 +2,7 @@ import { execSync, spawn } from "child_process";
2
2
  import { mkdirSync } from "fs";
3
3
  import { dirname, resolve } from "path";
4
4
  import { createInterface } from "readline";
5
+ import { runChromeScript } from "./chrome.js";
5
6
  export function registerRecordCommand(program) {
6
7
  program
7
8
  .command("record")
@@ -13,6 +14,8 @@ export function registerRecordCommand(program) {
13
14
  .option("-d, --duration <seconds>", "Recording duration in seconds")
14
15
  .option("-q, --quality <value>", "Quality: number for CRF, number+unit for bitrate (e.g. 23, 4000k, 4M)")
15
16
  .option("-f, --fps <fps>", "Frame rate")
17
+ .option("-n, --no-pointer", "Hide the mouse pointer")
18
+ .option("-t, --title <text>", "Select a window whose page title contains this text")
16
19
  .option("-v, --verbose", "Show ffmpeg output")
17
20
  .action(async (options) => {
18
21
  const missing = ["powershell.exe", "ffmpeg.exe", "wslpath"].filter((cmd) => {
@@ -27,15 +30,10 @@ export function registerRecordCommand(program) {
27
30
  if (missing.length > 0) {
28
31
  throw new Error(`Command(s) not found: ${missing.join(", ")}\nWSL + Windows environment required.`);
29
32
  }
30
- const localAppDataWin = execSync("powershell.exe -Command '$env:LOCALAPPDATA'")
31
- .toString()
32
- .trim();
33
- const ps1Win = `${localAppDataWin}\\y11i-3d\\chrome-recording\\get_chrome_info.ps1`;
34
- const info = execSync(`powershell.exe -ExecutionPolicy Bypass -File "${ps1Win}"`)
35
- .toString()
36
- .trim()
37
- .split("\n")
38
- .map((l) => l.trim().replace(/\r/, ""));
33
+ const infoArgs = ["info"];
34
+ if (options.title)
35
+ infoArgs.push("-Title", options.title);
36
+ const info = runChromeScript(infoArgs);
39
37
  const contentLine = info.find((l) => l.startsWith("content"));
40
38
  if (!contentLine) {
41
39
  throw new Error("Failed to get Chrome content area info");
@@ -102,6 +100,7 @@ export function registerRecordCommand(program) {
102
100
  `offset_y=${offsetY}`,
103
101
  `video_size=${contentW}x${contentH}`,
104
102
  ...(options.fps !== undefined ? [`framerate=${options.fps}`] : []),
103
+ ...(options.pointer ? [] : ["draw_mouse=0"]),
105
104
  ];
106
105
  const ddagrab = ddagrabParams.join(":");
107
106
  const ffmpegArgs = screenshot
@@ -119,6 +118,8 @@ export function registerRecordCommand(program) {
119
118
  ...(duration !== null ? ["-t", String(duration)] : []),
120
119
  "-c:v",
121
120
  "libx264",
121
+ "-pix_fmt",
122
+ "yuv420p",
122
123
  ...qualityArgs,
123
124
  "-y",
124
125
  outputWin,
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerResizeCommand(program: Command): void;
package/dist/resize.js ADDED
@@ -0,0 +1,29 @@
1
+ import { parseSize, runChromeScript } from "./chrome.js";
2
+ export function registerResizeCommand(program) {
3
+ program
4
+ .command("resize")
5
+ .description("Resize the Chrome viewport")
6
+ .argument("<size>", "Viewport size as <width>x<height>")
7
+ .option("-p, --padding <width>x<height>", "Padding kept outside the viewport (right and bottom)", "0x0")
8
+ .option("-t, --title <text>", "Select a page whose title contains this text")
9
+ .action((size, options) => {
10
+ const viewport = parseSize(size, "size");
11
+ const padding = parseSize(options.padding, "padding");
12
+ const args = [
13
+ "resize",
14
+ "-ViewportWidth",
15
+ String(viewport.width),
16
+ "-ViewportHeight",
17
+ String(viewport.height),
18
+ "-PaddingWidth",
19
+ String(padding.width),
20
+ "-PaddingHeight",
21
+ String(padding.height),
22
+ ];
23
+ if (options.title)
24
+ args.push("-Title", options.title);
25
+ for (const line of runChromeScript(args)) {
26
+ console.log(line);
27
+ }
28
+ });
29
+ }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "windows",
9
9
  "wsl"
10
10
  ],
11
- "version": "1.0.0",
11
+ "version": "1.1.0",
12
12
  "type": "module",
13
13
  "files": [
14
14
  "dist"
@@ -17,7 +17,7 @@
17
17
  "chrome-recording": "dist/cli.js"
18
18
  },
19
19
  "scripts": {
20
- "build": "rm -rf dist && tsc && cp src/get_chrome_info.ps1 dist/",
20
+ "build": "rm -rf dist && tsc && cp src/chrome.ps1 dist/",
21
21
  "postinstall": "node dist/postinstall.js"
22
22
  },
23
23
  "devDependencies": {
@@ -1,62 +0,0 @@
1
- Add-Type -AssemblyName System.Windows.Forms
2
-
3
- Add-Type -TypeDefinition @"
4
- using System;
5
- using System.Collections.Generic;
6
- using System.Runtime.InteropServices;
7
- using System.Text;
8
- public class Win32 {
9
- public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
10
- [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
11
- [DllImport("user32.dll")] public static extern int GetClassName(IntPtr hwnd, StringBuilder buf, int maxCount);
12
- [DllImport("user32.dll")] public static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc cb, IntPtr lParam);
13
- public struct RECT { public int Left, Top, Right, Bottom; }
14
-
15
- public static List<RECT> FindContentRects(IntPtr chromeHwnd) {
16
- RECT mainRect;
17
- GetWindowRect(chromeHwnd, out mainRect);
18
- var results = new List<RECT>();
19
- EnumChildWindows(chromeHwnd, (h, _) => {
20
- var buf = new StringBuilder(256);
21
- GetClassName(h, buf, 256);
22
- if (buf.ToString() == "Chrome_RenderWidgetHostHWND") {
23
- RECT r;
24
- GetWindowRect(h, out r);
25
- if (r.Top >= mainRect.Top && r.Top < mainRect.Bottom) {
26
- results.Add(r);
27
- }
28
- }
29
- return true;
30
- }, IntPtr.Zero);
31
- return results;
32
- }
33
- }
34
- "@
35
-
36
- $proc = Get-Process chrome -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -ne "" } | Select-Object -First 1
37
- if (-not $proc) {
38
- Write-Error "Chrome is not running or has no visible window."
39
- exit 1
40
- }
41
-
42
- $rects = [Win32]::FindContentRects($proc.MainWindowHandle)
43
- $best = $rects | Sort-Object { $_.Left }, { $_.Top } | Select-Object -First 1
44
-
45
- if (-not $best) {
46
- Write-Error "Chrome content area (Chrome_RenderWidgetHostHWND) not found."
47
- exit 1
48
- }
49
-
50
- $x = $best.Left
51
- $y = $best.Top
52
- $w = $best.Right - $best.Left
53
- $h = $best.Bottom - $best.Top
54
-
55
- Write-Output "content $x $y $w $h"
56
-
57
- $i = 0
58
- foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
59
- $b = $s.Bounds
60
- Write-Output "monitor $i $($b.Left) $($b.Top) $($b.Right) $($b.Bottom)"
61
- $i++
62
- }