@hyperframes/engine 0.7.60 → 0.7.61
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 +3 -4
- package/dist/config.d.ts +45 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -16
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +19 -3
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/browserLeasePool.d.ts +48 -0
- package/dist/services/browserLeasePool.d.ts.map +1 -0
- package/dist/services/browserLeasePool.js +210 -0
- package/dist/services/browserLeasePool.js.map +1 -0
- package/dist/services/browserManager.d.ts +6 -7
- package/dist/services/browserManager.d.ts.map +1 -1
- package/dist/services/browserManager.js +122 -177
- package/dist/services/browserManager.js.map +1 -1
- package/dist/services/chunkEncoder.d.ts.map +1 -1
- package/dist/services/chunkEncoder.js +45 -134
- package/dist/services/chunkEncoder.js.map +1 -1
- package/dist/services/frameCapture.d.ts +13 -1
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +85 -8
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/streamingEncoder.d.ts.map +1 -1
- package/dist/services/streamingEncoder.js +23 -58
- package/dist/services/streamingEncoder.js.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +39 -71
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/utils/ffmpegBinaries.d.ts +2 -2
- package/dist/utils/ffmpegBinaries.d.ts.map +1 -1
- package/dist/utils/ffmpegBinaries.js +8 -82
- package/dist/utils/ffmpegBinaries.js.map +1 -1
- package/dist/utils/ffprobe.d.ts +3 -1
- package/dist/utils/ffprobe.d.ts.map +1 -1
- package/dist/utils/ffprobe.js +35 -42
- package/dist/utils/ffprobe.js.map +1 -1
- package/dist/utils/gpuEncoder.d.ts.map +1 -1
- package/dist/utils/gpuEncoder.js +32 -52
- package/dist/utils/gpuEncoder.js.map +1 -1
- package/dist/utils/managedChildProcess.d.ts +50 -0
- package/dist/utils/managedChildProcess.d.ts.map +1 -0
- package/dist/utils/managedChildProcess.js +148 -0
- package/dist/utils/managedChildProcess.js.map +1 -0
- package/dist/utils/processTracker.js +1 -1
- package/dist/utils/runFfmpeg.d.ts +3 -0
- package/dist/utils/runFfmpeg.d.ts.map +1 -1
- package/dist/utils/runFfmpeg.js +16 -50
- package/dist/utils/runFfmpeg.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
function freezeFingerprint(fingerprint) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
...fingerprint,
|
|
4
|
+
args: Object.freeze([...fingerprint.args]),
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
function fingerprintKey(fingerprint) {
|
|
8
|
+
return JSON.stringify([
|
|
9
|
+
fingerprint.args,
|
|
10
|
+
fingerprint.executablePath ?? null,
|
|
11
|
+
fingerprint.browserTimeoutMs,
|
|
12
|
+
fingerprint.protocolTimeoutMs,
|
|
13
|
+
fingerprint.requestedCaptureMode,
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
/** Owns pooled browser generations and hands callers exactly-once leases. */
|
|
17
|
+
export class BrowserLeasePool {
|
|
18
|
+
options;
|
|
19
|
+
available = new Map();
|
|
20
|
+
entries = new Set();
|
|
21
|
+
leasesByBrowser = new Map();
|
|
22
|
+
drainPromise = null;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
async acquire(fingerprintInput, pooled) {
|
|
27
|
+
if (this.drainPromise)
|
|
28
|
+
await this.drainPromise;
|
|
29
|
+
const fingerprint = freezeFingerprint(fingerprintInput);
|
|
30
|
+
const entry = this.reserveEntry(fingerprint, pooled);
|
|
31
|
+
return this.issueLease(entry);
|
|
32
|
+
}
|
|
33
|
+
async issueLease(entry) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await entry.launchPromise;
|
|
36
|
+
if (entry.state !== "ready") {
|
|
37
|
+
throw new Error("Browser pool drained during acquisition");
|
|
38
|
+
}
|
|
39
|
+
return this.createLease(entry, result);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
reserveEntry(fingerprint, pooled) {
|
|
47
|
+
const key = fingerprintKey(fingerprint);
|
|
48
|
+
let entry = pooled ? this.available.get(key) : undefined;
|
|
49
|
+
if (entry?.state === "ready" && !entry.result?.browser.connected) {
|
|
50
|
+
this.requestClose(entry, true);
|
|
51
|
+
entry = undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!entry || entry.state === "closing")
|
|
54
|
+
return this.createEntry(key, fingerprint, pooled);
|
|
55
|
+
entry.refCount += 1;
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
/** Preserve legacy release-by-handle only when ownership is unambiguous. */
|
|
59
|
+
async releaseByBrowser(browser) {
|
|
60
|
+
const leases = this.leasesByBrowser.get(browser);
|
|
61
|
+
if (leases?.size === 1) {
|
|
62
|
+
const lease = leases.values().next().value;
|
|
63
|
+
await lease.release();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (leases) {
|
|
67
|
+
throw new Error(`Cannot release a pooled browser by handle while ${leases.size} leases are active; release the owning BrowserLease instead`);
|
|
68
|
+
}
|
|
69
|
+
await this.options.close(browser).catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
/** Preserve the legacy ambiguous-handle no-op without closing another owner's browser. */
|
|
72
|
+
forceReleaseByBrowser(browser) {
|
|
73
|
+
const leases = this.leasesByBrowser.get(browser);
|
|
74
|
+
if (leases?.size === 1) {
|
|
75
|
+
const lease = leases.values().next().value;
|
|
76
|
+
lease.forceRelease();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (leases)
|
|
80
|
+
return;
|
|
81
|
+
this.options.forceClose(browser);
|
|
82
|
+
}
|
|
83
|
+
drain() {
|
|
84
|
+
if (this.drainPromise)
|
|
85
|
+
return this.drainPromise;
|
|
86
|
+
const entries = [...this.entries];
|
|
87
|
+
for (const entry of entries)
|
|
88
|
+
this.requestClose(entry, false);
|
|
89
|
+
this.drainPromise = Promise.all(entries.map((entry) => entry.closePromise)).then(() => {
|
|
90
|
+
this.drainPromise = null;
|
|
91
|
+
});
|
|
92
|
+
return this.drainPromise;
|
|
93
|
+
}
|
|
94
|
+
/** Test-only state reset. Call `drain()` first when entries own real browsers. */
|
|
95
|
+
reset() {
|
|
96
|
+
this.available.clear();
|
|
97
|
+
this.entries.clear();
|
|
98
|
+
this.leasesByBrowser.clear();
|
|
99
|
+
this.drainPromise = null;
|
|
100
|
+
}
|
|
101
|
+
createEntry(key, fingerprint, pooled) {
|
|
102
|
+
const entry = {
|
|
103
|
+
key,
|
|
104
|
+
fingerprint,
|
|
105
|
+
pooled,
|
|
106
|
+
state: "launching",
|
|
107
|
+
refCount: 1,
|
|
108
|
+
closeRequested: false,
|
|
109
|
+
forceCloseRequested: false,
|
|
110
|
+
launchPromise: undefined,
|
|
111
|
+
};
|
|
112
|
+
entry.launchPromise = this.options.launch(fingerprint).then((result) => {
|
|
113
|
+
entry.result = result;
|
|
114
|
+
if (entry.closeRequested) {
|
|
115
|
+
entry.state = "closing";
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
entry.state = "ready";
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}, (error) => {
|
|
122
|
+
if (this.available.get(key) === entry)
|
|
123
|
+
this.available.delete(key);
|
|
124
|
+
this.entries.delete(entry);
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
127
|
+
this.entries.add(entry);
|
|
128
|
+
if (pooled)
|
|
129
|
+
this.available.set(key, entry);
|
|
130
|
+
return entry;
|
|
131
|
+
}
|
|
132
|
+
createLease(entry, result) {
|
|
133
|
+
let active = true;
|
|
134
|
+
let lease;
|
|
135
|
+
const deactivate = (force) => {
|
|
136
|
+
if (!active)
|
|
137
|
+
return false;
|
|
138
|
+
active = false;
|
|
139
|
+
this.removeLease(result.browser, lease);
|
|
140
|
+
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
141
|
+
if (entry.refCount === 0)
|
|
142
|
+
this.requestClose(entry, force);
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
145
|
+
lease = {
|
|
146
|
+
...result,
|
|
147
|
+
fingerprint: entry.fingerprint,
|
|
148
|
+
release: async () => {
|
|
149
|
+
if (!deactivate(false))
|
|
150
|
+
return;
|
|
151
|
+
await entry.closePromise;
|
|
152
|
+
},
|
|
153
|
+
forceRelease: () => {
|
|
154
|
+
if (!deactivate(true) && entry.refCount === 0 && entry.closePromise) {
|
|
155
|
+
this.requestClose(entry, true);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const leases = this.leasesByBrowser.get(result.browser) ?? new Set();
|
|
160
|
+
leases.add(lease);
|
|
161
|
+
this.leasesByBrowser.set(result.browser, leases);
|
|
162
|
+
return lease;
|
|
163
|
+
}
|
|
164
|
+
requestClose(entry, force) {
|
|
165
|
+
if (force) {
|
|
166
|
+
entry.forceCloseRequested = true;
|
|
167
|
+
entry.forceClose?.();
|
|
168
|
+
}
|
|
169
|
+
if (entry.closePromise)
|
|
170
|
+
return;
|
|
171
|
+
entry.closeRequested = true;
|
|
172
|
+
if (this.available.get(entry.key) === entry)
|
|
173
|
+
this.available.delete(entry.key);
|
|
174
|
+
entry.closePromise = entry.launchPromise
|
|
175
|
+
.then(async (result) => {
|
|
176
|
+
entry.state = "closing";
|
|
177
|
+
if (entry.forceCloseRequested) {
|
|
178
|
+
this.options.forceClose(result.browser);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
await Promise.race([
|
|
182
|
+
this.options.close(result.browser).catch(() => { }),
|
|
183
|
+
new Promise((resolve) => {
|
|
184
|
+
entry.forceClose = () => {
|
|
185
|
+
entry.forceClose = undefined;
|
|
186
|
+
this.options.forceClose(result.browser);
|
|
187
|
+
resolve();
|
|
188
|
+
};
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.catch(() => { })
|
|
194
|
+
.finally(() => {
|
|
195
|
+
entry.forceClose = undefined;
|
|
196
|
+
this.entries.delete(entry);
|
|
197
|
+
if (entry.result)
|
|
198
|
+
this.leasesByBrowser.delete(entry.result.browser);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
removeLease(browser, lease) {
|
|
202
|
+
const leases = this.leasesByBrowser.get(browser);
|
|
203
|
+
if (!leases)
|
|
204
|
+
return;
|
|
205
|
+
leases.delete(lease);
|
|
206
|
+
if (leases.size === 0)
|
|
207
|
+
this.leasesByBrowser.delete(browser);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=browserLeasePool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserLeasePool.js","sourceRoot":"","sources":["../../src/services/browserLeasePool.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Browser, PuppeteerNode } from "puppeteer-core";
|
|
8
8
|
import { type EngineConfig } from "../config.js";
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
9
|
+
import { type BrowserLease, type CaptureMode } from "./browserLeasePool.js";
|
|
10
|
+
export { BrowserLeasePool } from "./browserLeasePool.js";
|
|
11
|
+
export type { BrowserLaunchFingerprint, BrowserLease, BrowserPoolState, CaptureMode, } from "./browserLeasePool.js";
|
|
12
|
+
export type AcquiredBrowser = BrowserLease;
|
|
14
13
|
/**
|
|
15
14
|
* Resolve chrome-headless-shell binary for deterministic BeginFrame rendering.
|
|
16
15
|
* Checks config.chromePath, then PRODUCER_HEADLESS_SHELL_PATH env var,
|
|
17
|
-
* then
|
|
16
|
+
* then the CLI browser override, HyperFrames' managed cache, and Puppeteer's cache.
|
|
18
17
|
*/
|
|
19
18
|
export declare function resolveHeadlessShellPath(config?: Partial<Pick<EngineConfig, "chromePath">>): string | undefined;
|
|
20
19
|
export declare const ENABLE_BROWSER_POOL: boolean;
|
|
@@ -41,7 +40,7 @@ export declare function resolveBrowserGpuMode(mode: EngineConfig["browserGpuMode
|
|
|
41
40
|
platform?: NodeJS.Platform;
|
|
42
41
|
}): Promise<"software" | "hardware">;
|
|
43
42
|
export declare function acquireBrowser(chromeArgs: string[], config?: Partial<Pick<EngineConfig, "browserTimeout" | "protocolTimeout" | "enableBrowserPool" | "chromePath" | "forceScreenshot">>): Promise<AcquiredBrowser>;
|
|
44
|
-
export declare function releaseBrowser(browser: Browser,
|
|
43
|
+
export declare function releaseBrowser(browser: Browser, _config?: Partial<Pick<EngineConfig, "enableBrowserPool">>): Promise<void>;
|
|
45
44
|
export declare function forceReleaseBrowser(browser: Browser): void;
|
|
46
45
|
/**
|
|
47
46
|
* Forcefully close the pooled browser if one exists, regardless of refCount.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserManager.d.ts","sourceRoot":"","sources":["../../src/services/browserManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK7D,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"browserManager.d.ts","sourceRoot":"","sources":["../../src/services/browserManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK7D,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,wBAAwB,EACxB,YAAY,EACZ,gBAAgB,EAChB,WAAW,GACZ,MAAM,uBAAuB,CAAC;AAqF/B,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC;AA4C3C;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,GACjD,MAAM,GAAG,SAAS,CA8BpB;AAGD,eAAO,MAAM,mBAAmB,SAAmC,CAAC;AA2EpE,gDAAgD;AAChD,wBAAgB,qCAAqC,IAAI,IAAI,CAE5D;AAkED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,CAAC,gBAAgB,CAAC,EACpC,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACvB,GACL,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC,CAMlC;AAsCD,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,CACd,IAAI,CACF,YAAY,EACZ,gBAAgB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,YAAY,GAAG,iBAAiB,CAC9F,CACF,GACA,OAAO,CAAC,eAAe,CAAC,CAG1B;AAgFD,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC,GACzD,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE1D;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtD;AAED,uCAAuC;AACvC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI,CAE3E;AAyCD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,gBAAgB,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GACnF,MAAM,EAAE,CA0FV"}
|
|
@@ -10,6 +10,8 @@ import { join } from "path";
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { DEFAULT_CONFIG } from "../config.js";
|
|
12
12
|
import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.js";
|
|
13
|
+
import { BrowserLeasePool, } from "./browserLeasePool.js";
|
|
14
|
+
export { BrowserLeasePool } from "./browserLeasePool.js";
|
|
13
15
|
let _puppeteer;
|
|
14
16
|
function isSoftwareWebGlRenderer(rendererInfo) {
|
|
15
17
|
const renderer = rendererInfo.trim().toLowerCase();
|
|
@@ -76,28 +78,33 @@ async function probeHardwareWebGlInfo(ppt, options) {
|
|
|
76
78
|
await probeBrowser?.close().catch(() => { });
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
89
|
-
const envPath = process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
|
90
|
-
if (!existsSync(envPath)) {
|
|
91
|
-
throw new Error(`[BrowserManager] Chrome binary not found at PRODUCER_HEADLESS_SHELL_PATH="${envPath}". ` +
|
|
92
|
-
"Run `hyperframes browser ensure` to re-download.");
|
|
81
|
+
function compareBrowserVersionsDescending(left, right) {
|
|
82
|
+
const parse = (value) => {
|
|
83
|
+
const version = value.slice(value.indexOf("-") + 1);
|
|
84
|
+
const segments = [];
|
|
85
|
+
for (const segment of version.split(".")) {
|
|
86
|
+
const parsed = Number.parseInt(segment, 10);
|
|
87
|
+
if (!Number.isFinite(parsed))
|
|
88
|
+
break;
|
|
89
|
+
segments.push(parsed);
|
|
93
90
|
}
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
const
|
|
91
|
+
return segments;
|
|
92
|
+
};
|
|
93
|
+
const leftSegments = parse(left);
|
|
94
|
+
const rightSegments = parse(right);
|
|
95
|
+
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
96
|
+
for (let index = 0; index < length; index += 1) {
|
|
97
|
+
const difference = (rightSegments[index] ?? 0) - (leftSegments[index] ?? 0);
|
|
98
|
+
if (difference !== 0)
|
|
99
|
+
return difference;
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
function findCachedHeadlessShell(baseDir) {
|
|
97
104
|
if (!existsSync(baseDir))
|
|
98
105
|
return undefined;
|
|
99
106
|
try {
|
|
100
|
-
const versions = readdirSync(baseDir).sort()
|
|
107
|
+
const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending);
|
|
101
108
|
for (const version of versions) {
|
|
102
109
|
const candidates = [
|
|
103
110
|
join(baseDir, version, "chrome-headless-shell-linux64", "chrome-headless-shell"),
|
|
@@ -112,14 +119,38 @@ export function resolveHeadlessShellPath(config) {
|
|
|
112
119
|
}
|
|
113
120
|
}
|
|
114
121
|
catch {
|
|
115
|
-
//
|
|
122
|
+
// Ignore unreadable cache directories and continue browser discovery.
|
|
116
123
|
}
|
|
117
124
|
return undefined;
|
|
118
125
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Resolve chrome-headless-shell binary for deterministic BeginFrame rendering.
|
|
128
|
+
* Checks config.chromePath, then PRODUCER_HEADLESS_SHELL_PATH env var,
|
|
129
|
+
* then the CLI browser override, HyperFrames' managed cache, and Puppeteer's cache.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveHeadlessShellPath(config) {
|
|
132
|
+
if (config?.chromePath) {
|
|
133
|
+
return config.chromePath;
|
|
134
|
+
}
|
|
135
|
+
if (process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
136
|
+
const envPath = process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
|
137
|
+
if (!existsSync(envPath)) {
|
|
138
|
+
throw new Error(`[BrowserManager] Chrome binary not found at PRODUCER_HEADLESS_SHELL_PATH="${envPath}". ` +
|
|
139
|
+
"Run `hyperframes browser ensure` to re-download.");
|
|
140
|
+
}
|
|
141
|
+
return envPath;
|
|
142
|
+
}
|
|
143
|
+
if (process.env.HYPERFRAMES_BROWSER_PATH) {
|
|
144
|
+
const envPath = process.env.HYPERFRAMES_BROWSER_PATH;
|
|
145
|
+
if (!existsSync(envPath)) {
|
|
146
|
+
throw new Error(`[BrowserManager] Chrome binary not found at HYPERFRAMES_BROWSER_PATH="${envPath}". ` +
|
|
147
|
+
"Run `hyperframes browser ensure` to re-download.");
|
|
148
|
+
}
|
|
149
|
+
return envPath;
|
|
150
|
+
}
|
|
151
|
+
const home = homedir();
|
|
152
|
+
return (findCachedHeadlessShell(join(home, ".cache", "hyperframes", "chrome", "chrome-headless-shell")) ?? findCachedHeadlessShell(join(home, ".cache", "puppeteer", "chrome-headless-shell")));
|
|
153
|
+
}
|
|
123
154
|
// Preserve the producer-era export so re-export shims keep the same public API.
|
|
124
155
|
export const ENABLE_BROWSER_POOL = DEFAULT_CONFIG.enableBrowserPool;
|
|
125
156
|
// Flags only meaningful when Chrome's compositor is driven by
|
|
@@ -284,153 +315,71 @@ export function resolveBrowserGpuMode(mode, options = {}) {
|
|
|
284
315
|
function logResolvedBrowserGpuMode(resolved, reason) {
|
|
285
316
|
console.error(`[hyperframes] browserGpuMode auto → ${resolved} (${reason})`);
|
|
286
317
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
function createBrowserLaunchFingerprint(chromeArgs, config) {
|
|
319
|
+
const launchConfig = {
|
|
320
|
+
browserTimeout: DEFAULT_CONFIG.browserTimeout,
|
|
321
|
+
protocolTimeout: DEFAULT_CONFIG.protocolTimeout,
|
|
322
|
+
forceScreenshot: DEFAULT_CONFIG.forceScreenshot,
|
|
323
|
+
...config,
|
|
324
|
+
};
|
|
325
|
+
const headlessShell = resolveHeadlessShellPath(launchConfig);
|
|
326
|
+
const requestedCaptureMode = headlessShell && process.platform === "linux" && !launchConfig.forceScreenshot
|
|
327
|
+
? "beginframe"
|
|
328
|
+
: "screenshot";
|
|
329
|
+
return {
|
|
330
|
+
args: chromeArgs,
|
|
331
|
+
executablePath: headlessShell,
|
|
332
|
+
browserTimeoutMs: launchConfig.browserTimeout,
|
|
333
|
+
protocolTimeoutMs: launchConfig.protocolTimeout,
|
|
334
|
+
requestedCaptureMode,
|
|
335
|
+
};
|
|
300
336
|
}
|
|
301
337
|
export async function acquireBrowser(chromeArgs, config) {
|
|
302
338
|
const enablePool = config?.enableBrowserPool ?? DEFAULT_CONFIG.enableBrowserPool;
|
|
303
|
-
|
|
304
|
-
if (!pooledBrowser.connected) {
|
|
305
|
-
pooledBrowser = null;
|
|
306
|
-
pooledBrowserRefCount = 0;
|
|
307
|
-
_pooledBrowserLaunchPromise = null;
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
// Validate mode compatibility: a caller that needs screenshot mode
|
|
311
|
-
// (forceScreenshot, alpha output, BeginFrame timeout retry) must not
|
|
312
|
-
// receive a beginframe browser — the BeginFrame-only flags make the
|
|
313
|
-
// compositor wait for frames the screenshot path never sends.
|
|
314
|
-
const requestedMode = resolveRequestedCaptureMode(config);
|
|
315
|
-
if (pooledCaptureMode === requestedMode) {
|
|
316
|
-
pooledBrowserRefCount += 1;
|
|
317
|
-
return { browser: pooledBrowser, captureMode: pooledCaptureMode };
|
|
318
|
-
}
|
|
319
|
-
// Mode mismatch — skip pool, launch a dedicated browser for this caller.
|
|
320
|
-
// Don't evict the pooled browser: other sessions may still hold refs.
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
// Dedup concurrent launches: when the pool is enabled and multiple callers
|
|
324
|
-
// (e.g. parallel workers via Promise.all) race into acquireBrowser before
|
|
325
|
-
// the first launch completes, they would all see pooledBrowser === null and
|
|
326
|
-
// each spawn a separate Chrome. Cache the in-flight launch Promise so the
|
|
327
|
-
// second+ callers await the same one instead of launching again.
|
|
328
|
-
if (enablePool && _pooledBrowserLaunchPromise) {
|
|
329
|
-
const result = await _pooledBrowserLaunchPromise;
|
|
330
|
-
const requestedMode = resolveRequestedCaptureMode(config);
|
|
331
|
-
if (result.captureMode === requestedMode) {
|
|
332
|
-
pooledBrowserRefCount += 1;
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
|
-
// Mode mismatch with pending launch — launch a dedicated browser.
|
|
336
|
-
}
|
|
337
|
-
const launchPromise = launchBrowser(chromeArgs, config);
|
|
338
|
-
if (enablePool && !pooledBrowser && !_pooledBrowserLaunchPromise) {
|
|
339
|
-
_pooledBrowserLaunchPromise = launchPromise;
|
|
340
|
-
try {
|
|
341
|
-
const result = await launchPromise;
|
|
342
|
-
pooledBrowser = result.browser;
|
|
343
|
-
pooledBrowserRefCount = 1;
|
|
344
|
-
pooledCaptureMode = result.captureMode;
|
|
345
|
-
return result;
|
|
346
|
-
}
|
|
347
|
-
finally {
|
|
348
|
-
_pooledBrowserLaunchPromise = null;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return launchPromise;
|
|
339
|
+
return browserLeasePool.acquire(createBrowserLaunchFingerprint(chromeArgs, config), enablePool);
|
|
352
340
|
}
|
|
353
341
|
// fallow-ignore-next-line complexity
|
|
354
|
-
async function launchBrowser(
|
|
355
|
-
// Config chromePath overrides env var / auto-detection.
|
|
356
|
-
const headlessShell = resolveHeadlessShellPath(config);
|
|
357
|
-
// BeginFrame requires chrome-headless-shell AND Linux (crashes on
|
|
358
|
-
// macOS/Windows — crbug.com/40656275).
|
|
359
|
-
const isLinux = process.platform === "linux";
|
|
360
|
-
const forceScreenshot = config?.forceScreenshot ?? DEFAULT_CONFIG.forceScreenshot;
|
|
361
|
-
let captureMode;
|
|
362
|
-
let executablePath;
|
|
363
|
-
if (headlessShell && isLinux && !forceScreenshot) {
|
|
364
|
-
captureMode = "beginframe";
|
|
365
|
-
executablePath = headlessShell;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
// Screenshot mode with renderSeek: works on all platforms.
|
|
369
|
-
captureMode = "screenshot";
|
|
370
|
-
executablePath = headlessShell ?? undefined;
|
|
371
|
-
}
|
|
342
|
+
async function launchBrowser(fingerprint) {
|
|
372
343
|
const ppt = await getPuppeteer();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
344
|
+
let captureMode = fingerprint.requestedCaptureMode;
|
|
345
|
+
let browser;
|
|
346
|
+
try {
|
|
347
|
+
browser = await ppt.launch({
|
|
348
|
+
headless: true,
|
|
349
|
+
args: [...fingerprint.args],
|
|
350
|
+
defaultViewport: null,
|
|
351
|
+
executablePath: fingerprint.executablePath,
|
|
352
|
+
timeout: fingerprint.browserTimeoutMs,
|
|
353
|
+
protocolTimeout: fingerprint.protocolTimeoutMs,
|
|
354
|
+
});
|
|
355
|
+
const browserVersion = await browser.version().catch(() => "unknown");
|
|
356
|
+
const gpuFlags = fingerprint.args.filter((a) => a.startsWith("--use-gl=") || a.startsWith("--use-angle="));
|
|
357
|
+
console.log(`[BrowserManager] Browser launched (${browserVersion}, ${captureMode}, gl=${gpuFlags.join(" ") || "default"}, headlessShell=${!!fingerprint.executablePath}, platform=${process.platform})`);
|
|
358
|
+
if (captureMode === "beginframe") {
|
|
359
|
+
const supported = await probeBeginFrameSupport(browser).catch(() => true);
|
|
360
|
+
if (!supported) {
|
|
361
|
+
await browser.close().catch(() => { });
|
|
362
|
+
browser = undefined;
|
|
363
|
+
console.warn("[BrowserManager] HeadlessExperimental.beginFrame unavailable in this Chromium build; falling back to screenshot mode.");
|
|
364
|
+
captureMode = "screenshot";
|
|
365
|
+
browser = await ppt.launch({
|
|
366
|
+
headless: true,
|
|
367
|
+
args: stripBeginFrameFlags([...fingerprint.args]),
|
|
368
|
+
defaultViewport: null,
|
|
369
|
+
executablePath: fingerprint.executablePath,
|
|
370
|
+
timeout: fingerprint.browserTimeoutMs,
|
|
371
|
+
protocolTimeout: fingerprint.protocolTimeoutMs,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
400
374
|
}
|
|
375
|
+
return { browser, captureMode };
|
|
401
376
|
}
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const enablePool = config?.enableBrowserPool ?? DEFAULT_CONFIG.enableBrowserPool;
|
|
406
|
-
if (!enablePool) {
|
|
407
|
-
await browser.close().catch(() => { });
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
if (pooledBrowser && pooledBrowser === browser) {
|
|
411
|
-
pooledBrowserRefCount = Math.max(0, pooledBrowserRefCount - 1);
|
|
412
|
-
if (pooledBrowserRefCount === 0) {
|
|
413
|
-
await browser.close().catch(() => { });
|
|
414
|
-
pooledBrowser = null;
|
|
415
|
-
_pooledBrowserLaunchPromise = null;
|
|
416
|
-
}
|
|
417
|
-
return;
|
|
377
|
+
catch (error) {
|
|
378
|
+
await browser?.close().catch(() => { });
|
|
379
|
+
throw error;
|
|
418
380
|
}
|
|
419
|
-
await browser.close().catch(() => { });
|
|
420
381
|
}
|
|
421
|
-
|
|
422
|
-
if (pooledBrowser && pooledBrowser === browser) {
|
|
423
|
-
// If other sessions still hold refs, just drop ours — don't kill the
|
|
424
|
-
// shared Chrome out from under them. The browser will be cleaned up when
|
|
425
|
-
// the last session releases or drainBrowserPool is called.
|
|
426
|
-
if (pooledBrowserRefCount > 1) {
|
|
427
|
-
pooledBrowserRefCount -= 1;
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
pooledBrowserRefCount = 0;
|
|
431
|
-
pooledBrowser = null;
|
|
432
|
-
_pooledBrowserLaunchPromise = null;
|
|
433
|
-
}
|
|
382
|
+
function forceCloseBrowserProcess(browser) {
|
|
434
383
|
const proc = browser.process?.();
|
|
435
384
|
if (proc && !proc.killed) {
|
|
436
385
|
try {
|
|
@@ -447,32 +396,28 @@ export function forceReleaseBrowser(browser) {
|
|
|
447
396
|
// Best-effort cleanup.
|
|
448
397
|
}
|
|
449
398
|
}
|
|
399
|
+
const browserLeasePool = new BrowserLeasePool({
|
|
400
|
+
launch: launchBrowser,
|
|
401
|
+
close: async (browser) => browser.close(),
|
|
402
|
+
forceClose: forceCloseBrowserProcess,
|
|
403
|
+
});
|
|
404
|
+
export async function releaseBrowser(browser, _config) {
|
|
405
|
+
await browserLeasePool.releaseByBrowser(browser);
|
|
406
|
+
}
|
|
407
|
+
export function forceReleaseBrowser(browser) {
|
|
408
|
+
browserLeasePool.forceReleaseByBrowser(browser);
|
|
409
|
+
}
|
|
450
410
|
/**
|
|
451
411
|
* Forcefully close the pooled browser if one exists, regardless of refCount.
|
|
452
412
|
* Used for explicit cleanup at process exit or between independent render jobs
|
|
453
413
|
* that should not share browser state.
|
|
454
414
|
*/
|
|
455
415
|
export async function drainBrowserPool() {
|
|
456
|
-
|
|
457
|
-
// drain and produces a browser that nobody references (orphan).
|
|
458
|
-
const pending = _pooledBrowserLaunchPromise;
|
|
459
|
-
_pooledBrowserLaunchPromise = null;
|
|
460
|
-
if (pending) {
|
|
461
|
-
await pending.then((r) => r.browser.close()).catch(() => { });
|
|
462
|
-
}
|
|
463
|
-
if (pooledBrowser) {
|
|
464
|
-
const browser = pooledBrowser;
|
|
465
|
-
pooledBrowser = null;
|
|
466
|
-
pooledBrowserRefCount = 0;
|
|
467
|
-
await browser.close().catch(() => { });
|
|
468
|
-
}
|
|
416
|
+
await browserLeasePool.drain();
|
|
469
417
|
}
|
|
470
418
|
/** Test-only: reset all pool state. */
|
|
471
419
|
export function _resetBrowserPoolForTests() {
|
|
472
|
-
|
|
473
|
-
pooledBrowserRefCount = 0;
|
|
474
|
-
pooledCaptureMode = "screenshot";
|
|
475
|
-
_pooledBrowserLaunchPromise = null;
|
|
420
|
+
browserLeasePool.reset();
|
|
476
421
|
}
|
|
477
422
|
/** Test-only: inject a mock PuppeteerNode so tests bypass the dynamic import. */
|
|
478
423
|
export function _setPuppeteerForTests(mock) {
|