@y11i-3d/chrome-recording 1.0.1 → 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 +39 -1
- package/dist/chrome.d.ts +5 -0
- package/dist/chrome.js +33 -0
- package/dist/chrome.ps1 +212 -0
- package/dist/cli.js +8 -0
- package/dist/postinstall.js +3 -3
- package/dist/record.js +8 -9
- package/dist/resize.d.ts +2 -0
- package/dist/resize.js +29 -0
- package/package.json +2 -2
- package/dist/get_chrome_info.ps1 +0 -62
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 (`
|
|
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).
|
package/dist/chrome.d.ts
ADDED
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
|
+
}
|
package/dist/chrome.ps1
ADDED
|
@@ -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();
|
package/dist/postinstall.js
CHANGED
|
@@ -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, "
|
|
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, "
|
|
19
|
-
console.log(`Installed
|
|
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
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
const
|
|
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
|
package/dist/resize.d.ts
ADDED
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
|
|
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/
|
|
20
|
+
"build": "rm -rf dist && tsc && cp src/chrome.ps1 dist/",
|
|
21
21
|
"postinstall": "node dist/postinstall.js"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
package/dist/get_chrome_info.ps1
DELETED
|
@@ -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
|
-
}
|